From 425b0a8ea64c992ed6d2254b56f4e715260d9249 Mon Sep 17 00:00:00 2001 From: ChatGPT Date: Sun, 6 Sep 2026 02:45:44 +0200 Subject: [PATCH 1/9] Add typed CAAM controls and provenance-aware observability models --- .../Host/Process/SubprocessRunner.swift | 15 +- .../Observability/NetdataReadOnlyFacade.swift | 39 +++ .../Observability/ObservabilityHub.swift | 241 ++++++++++++++++ .../Codex/CAAM/CAAMControlModels.swift | 183 ++++++++++++ .../Codex/CAAM/CAAMControlSession.swift | 270 ++++++++++++++++++ .../Codex/CAAM/CAAMControlValidation.swift | 124 ++++++++ .../Codex/CAAM/CAAMEnvironmentClient.swift | 115 +++++++- .../Codex/CAAM/CAAMEnvironmentModels.swift | 1 + .../CAAM/CAAMEnvironmentValidation.swift | 109 +++---- .../CAAM/CAAMOperationReceiptStore.swift | 66 +++++ TestsLinux/CAAMControlFixtures.swift | 93 ++++++ TestsLinux/CAAMControlSessionLinuxTests.swift | 166 +++++++++++ .../CAAMControlTransportLinuxTests.swift | 149 ++++++++++ .../CAAMEnvironmentContractLinuxTests.swift | 6 +- .../CAAMOperationReceiptLinuxTests.swift | 45 +++ TestsLinux/ObservabilityHubLinuxTests.swift | 124 ++++++++ docs/work-notes/caam-environment-control.md | 210 ++++++++++++++ 17 files changed, 1883 insertions(+), 73 deletions(-) create mode 100644 Sources/CodexBarCore/Observability/NetdataReadOnlyFacade.swift create mode 100644 Sources/CodexBarCore/Observability/ObservabilityHub.swift create mode 100644 Sources/CodexBarCore/Providers/Codex/CAAM/CAAMControlModels.swift create mode 100644 Sources/CodexBarCore/Providers/Codex/CAAM/CAAMControlSession.swift create mode 100644 Sources/CodexBarCore/Providers/Codex/CAAM/CAAMControlValidation.swift create mode 100644 Sources/CodexBarCore/Providers/Codex/CAAM/CAAMOperationReceiptStore.swift create mode 100644 TestsLinux/CAAMControlFixtures.swift create mode 100644 TestsLinux/CAAMControlSessionLinuxTests.swift create mode 100644 TestsLinux/CAAMControlTransportLinuxTests.swift create mode 100644 TestsLinux/CAAMOperationReceiptLinuxTests.swift create mode 100644 TestsLinux/ObservabilityHubLinuxTests.swift diff --git a/Sources/CodexBarCore/Host/Process/SubprocessRunner.swift b/Sources/CodexBarCore/Host/Process/SubprocessRunner.swift index 229fa74227..519e243de1 100644 --- a/Sources/CodexBarCore/Host/Process/SubprocessRunner.swift +++ b/Sources/CodexBarCore/Host/Process/SubprocessRunner.swift @@ -37,6 +37,15 @@ public enum SubprocessRunnerError: LocalizedError, Sendable { public struct SubprocessResult: Sendable { public let stdout: String public let stderr: String + public let exitCode: Int32 + public let stdoutWasValidUTF8: Bool + + public init(stdout: String, stderr: String, exitCode: Int32 = 0, stdoutWasValidUTF8: Bool = true) { + self.stdout = stdout + self.stderr = stderr + self.exitCode = exitCode + self.stdoutWasValidUTF8 = stdoutWasValidUTF8 + } } public enum SubprocessRunner { @@ -320,7 +329,11 @@ public enum SubprocessRunner { "status": "\(exitCode)", "duration_ms": "\(Int(duration * 1000))", ]) - return SubprocessResult(stdout: stdout, stderr: stderr) + return SubprocessResult( + stdout: stdout, + stderr: stderr, + exitCode: exitCode, + stdoutWasValidUTF8: String(data: capturedStdout, encoding: .utf8) != nil) } catch { let duration = Date().timeIntervalSince(start) self.log.warning( diff --git a/Sources/CodexBarCore/Observability/NetdataReadOnlyFacade.swift b/Sources/CodexBarCore/Observability/NetdataReadOnlyFacade.swift new file mode 100644 index 0000000000..007a4fa42f --- /dev/null +++ b/Sources/CodexBarCore/Observability/NetdataReadOnlyFacade.swift @@ -0,0 +1,39 @@ +import Foundation + +public struct HubHostObservation: Sendable, Equatable, Identifiable { + public let environmentID: String + public let evidence: HubEvidence + public let cpuPercent: Double? + public let memoryUsedBytes: UInt64? + + public var id: String { self.environmentID } +} + +/// No Netdata API contract is present in this repository. No network collector is activated or guessed. +public enum NetdataReadOnlyFacade { + public static func unavailable(environmentID: String) -> HubHostObservation { + HubHostObservation( + environmentID: environmentID, + evidence: HubEvidence(source: "Netdata collector not configured", observedAt: nil, availability: .unsupported), + cpuPercent: nil, + memoryUsedBytes: nil) + } + + /// A user-supplied navigation target is not telemetry evidence. Never attach credentials or query parameters. + public static func dashboardURL(_ value: String) -> URL? { + guard value.utf8.count <= 2048, + value == value.trimmingCharacters(in: .whitespacesAndNewlines), + !value.unicodeScalars.contains(where: { CharacterSet.controlCharacters.contains($0) }), + let components = URLComponents(string: value), + components.user == nil, components.password == nil, + components.query == nil, components.fragment == nil, + let host = components.host?.lowercased(), !host.isEmpty, + components.port == nil || (1...65535).contains(components.port ?? 0) + else { return nil } + let loopback = ["localhost", "127.0.0.1", "[::1]", "::1"].contains(host) + guard components.scheme?.lowercased() == "https" || + (components.scheme?.lowercased() == "http" && loopback) + else { return nil } + return components.url + } +} diff --git a/Sources/CodexBarCore/Observability/ObservabilityHub.swift b/Sources/CodexBarCore/Observability/ObservabilityHub.swift new file mode 100644 index 0000000000..32d52a76ec --- /dev/null +++ b/Sources/CodexBarCore/Observability/ObservabilityHub.swift @@ -0,0 +1,241 @@ +import Foundation + +public enum HubAvailability: String, Sendable, Equatable { + case available + case stale + case partial + case unavailable + case unsupported +} + +public struct HubEvidence: Sendable, Equatable { + public let source: String + public let observedAt: Date? + public let availability: HubAvailability + + public init(source: String, observedAt: Date?, availability: HubAvailability) { + self.source = String(String.UnicodeScalarView( + source.unicodeScalars.prefix(160).filter { !CharacterSet.controlCharacters.contains($0) })) + self.observedAt = observedAt + self.availability = availability + } + + public static func cached( + source: String, + observedAt: Date?, + failed: Bool, + partial: Bool = false, + now: Date) -> Self + { + guard let observedAt else { + return Self(source: source, observedAt: nil, availability: .unavailable) + } + let age = now.timeIntervalSince(observedAt) + let stale = failed || !age.isFinite || age < -5 || age > 300 + return Self(source: source, observedAt: observedAt, availability: stale ? .stale : partial ? .partial : .available) + } +} + +public struct HubAccountObservation: Sendable, Equatable, Identifiable { + public let environmentID: String + public let environmentLabel: String + public let profile: String + public let provider: String? + public let stableID: String? + public let displayLabel: String? + public let hostDefault: Bool + public let health: CAAMProfileHealth + public let evidence: HubEvidence + + public var id: String { "\(self.environmentID):\(self.profile)" } + + /// Correlation never joins profile names or display emails across environments/providers. + public func isSameAccount(as other: Self) -> Bool { + guard let provider, let stableID else { return false } + return provider == other.provider && stableID == other.stableID + } +} + +public struct HubServiceObservation: Sendable, Equatable, Identifiable { + public let row: CAAMEnvironmentRowState + public let runtimeEffectiveProfile: String? + public let pendingOperationID: UUID? + public let evidence: HubEvidence + + public var id: String { self.row.id } +} + +public struct HubCostObservation: Sendable, Equatable { + public let amount: Double? + public let meteredAmount: Double? + public let currency: String + public let historyDays: Int + public let provenance: CostProvenance + public let evidence: HubEvidence +} + +public struct HubProviderObservation: Sendable, Equatable, Identifiable { + public let id: String + public let label: String + public let primaryUsedPercent: Double? + public let windowMinutes: Int? + public let resetsAt: Date? + public let confidence: UsageDataConfidence + public let evidence: HubEvidence + public let cost: HubCostObservation? +} + +public struct HubWorkObservation: Sendable, Equatable, Identifiable { + public let id: String + public let provider: String + public let ordinal: Int + public let lastActivity: Date + public let tokens: Int? + public let estimatedCostUSD: Double? + public let evidence: HubEvidence +} + +public struct HubProviderInput: Sendable { + public let id: String + public let label: String + public let source: String + public let usage: UsageSnapshot? + public let cost: CostUsageTokenSnapshot? + public let usageFailed: Bool + public let costFailed: Bool + + public init( + id: String, + label: String, + source: String, + usage: UsageSnapshot?, + cost: CostUsageTokenSnapshot?, + usageFailed: Bool = false, + costFailed: Bool = false) + { + self.id = id + self.label = label + self.source = source + self.usage = usage + self.cost = cost + self.usageFailed = usageFailed + self.costFailed = costFailed + } +} + +/// Separate ledgers by construction: there is deliberately no combined utilization or spend total. +public struct ObservabilityHubSnapshot: Sendable, Equatable { + public let services: [HubServiceObservation] + public let accounts: [HubAccountObservation] + public let providers: [HubProviderObservation] + public let hosts: [HubHostObservation] + public let work: [HubWorkObservation] + public let workIsPartial: Bool +} + +public enum ObservabilityHub { + public static let maximumProviders = 64 + public static let maximumWorkRows = 100 + + public static func aggregate( + sessions: [CAAMControlSession], + providers: [HubProviderInput], + includeWork: Bool = false, + now: Date) -> ObservabilityHubSnapshot + { + let boundedSessions = sessions.prefix(CAAMEnvironmentContract.maximumEnvironmentCount) + let services = boundedSessions.map { session in + let row = session.row(now: now) + return HubServiceObservation( + row: row, + runtimeEffectiveProfile: session.cachedSnapshot(now: now)?.runtime.effectiveProfile, + pendingOperationID: session.pendingOperationID, + evidence: self.evidence(for: row)) + } + let accounts = boundedSessions.flatMap { session -> [HubAccountObservation] in + guard let snapshot = session.cachedSnapshot(now: now) else { return [] } + let evidence = self.evidence(for: session.row(now: now)) + return snapshot.profiles.prefix(CAAMEnvironmentContract.maximumProfileCount).map { profile in + HubAccountObservation( + environmentID: session.configuration.id, + environmentLabel: session.configuration.label, + profile: profile.name, + provider: profile.identity?.provider, + stableID: profile.identity?.stableID, + displayLabel: profile.identity?.displayEmail ?? profile.identity?.workspaceLabel, + hostDefault: profile.name == snapshot.hostDefaultProfile, + health: profile.health, + evidence: evidence) + } + } + let boundedProviders = providers.prefix(self.maximumProviders) + let providerRows = boundedProviders.map { input in + let primary = input.usage?.primary + let percent = primary?.isSyntheticPlaceholder == false ? self.nonnegative(primary?.usedPercent) : nil + let cost = input.cost.map { snapshot in + HubCostObservation( + amount: self.nonnegative(snapshot.last30DaysCostUSD), + meteredAmount: self.nonnegative(snapshot.meteredCostUSD), + currency: snapshot.currencyCode, + historyDays: snapshot.historyDays, + provenance: snapshot.costProvenance, + evidence: .cached( + source: "Existing provider cost cache", observedAt: snapshot.updatedAt, + failed: input.costFailed, + partial: !snapshot.historyCoverageIsEstablished || snapshot.costProvenance == .unknown, + now: now)) + } + return HubProviderObservation( + id: input.id, + label: input.label, + primaryUsedPercent: percent, + windowMinutes: primary?.windowMinutes, + resetsAt: primary?.resetsAt, + confidence: input.usage?.dataConfidence ?? .unknown, + evidence: .cached( + source: input.source.isEmpty ? "Existing provider snapshot" : input.source, + observedAt: input.usage?.updatedAt, failed: input.usageFailed, partial: percent == nil, now: now), + cost: cost) + } + var work: [HubWorkObservation] = [] + var workIsPartial = false + // Work details use only already-loaded records, and are not materialized for other tabs. + if includeWork { + for input in boundedProviders { + guard let cost = input.cost else { continue } + let available = self.maximumWorkRows - work.count + if cost.sessions.count > available { workIsPartial = true } + for (index, session) in cost.sessions.prefix(available).enumerated() { + work.append(HubWorkObservation( + id: "\(input.id):\(index)", provider: input.label, ordinal: index + 1, + lastActivity: session.lastActivity, + tokens: session.totalTokens.flatMap { $0 >= 0 ? $0 : nil }, + estimatedCostUSD: self.nonnegative(session.costUSD), + evidence: .cached( + source: "Recorded local session cache", observedAt: cost.updatedAt, + failed: input.costFailed, partial: true, now: now))) + } + } + } + return ObservabilityHubSnapshot( + services: services, accounts: accounts, providers: providerRows, + hosts: services.map { NetdataReadOnlyFacade.unavailable(environmentID: $0.id) }, + work: work, workIsPartial: workIsPartial) + } + + private static func nonnegative(_ value: Double?) -> Double? { + guard let value, value.isFinite, value >= 0 else { return nil } + return value + } + + private static func evidence(for row: CAAMEnvironmentRowState) -> HubEvidence { + let availability: HubAvailability = switch row.availability { + case .ready: .available + case .stale: .stale + case .degraded, .recoveryRequired: .partial + case .incompatible: .unsupported + case .unavailable: .unavailable + } + return HubEvidence(source: "CAAM gateway snapshot", observedAt: row.observedAt, availability: availability) + } +} diff --git a/Sources/CodexBarCore/Providers/Codex/CAAM/CAAMControlModels.swift b/Sources/CodexBarCore/Providers/Codex/CAAM/CAAMControlModels.swift new file mode 100644 index 0000000000..83523085f9 --- /dev/null +++ b/Sources/CodexBarCore/Providers/Codex/CAAM/CAAMControlModels.swift @@ -0,0 +1,183 @@ +import Foundation + +public struct CAAMSwitchPlan: Codable, Sendable, Equatable { + public let planDigest: String + public let expectedRevision: String + public let currentProfile: String? + public let targetProfile: String + public let affectedProfiles: [String] + public let reloadRequired: Bool + public let expiresAt: Date + + public init( + planDigest: String, + expectedRevision: String, + currentProfile: String?, + targetProfile: String, + affectedProfiles: [String], + reloadRequired: Bool, + expiresAt: Date) + { + self.planDigest = planDigest + self.expectedRevision = expectedRevision + self.currentProfile = currentProfile + self.targetProfile = targetProfile + self.affectedProfiles = affectedProfiles + self.reloadRequired = reloadRequired + self.expiresAt = expiresAt + } + + private enum CodingKeys: String, CodingKey { + case planDigest = "plan_digest" + case expectedRevision = "expected_revision" + case currentProfile = "current_profile" + case targetProfile = "target_profile" + case affectedProfiles = "affected_profiles" + case reloadRequired = "reload_required" + case expiresAt = "expires_at" + } +} + +public enum CAAMOperationState: String, Codable, Sendable { + case planned + case executing + case committed + case rejected + case rolledBack = "rolled_back" + case recoveryRequired = "recovery_required" + case manualRequired = "manual_required" + + public var isTerminal: Bool { + switch self { + case .committed, .rejected, .rolledBack: true + case .planned, .executing, .recoveryRequired, .manualRequired: false + } + } +} + +public struct CAAMOperationResult: Codable, Sendable, Equatable { + public let operationID: UUID + public let state: CAAMOperationState + public let effect: CAAMControlEffect + public let previousProfile: String? + public let currentProfile: String? + public let recoveryRequired: Bool + public let manualRequired: Bool + public let snapshot: CAAMEnvironmentSnapshot? + + public init( + operationID: UUID, + state: CAAMOperationState, + effect: CAAMControlEffect, + previousProfile: String? = nil, + currentProfile: String? = nil, + recoveryRequired: Bool = false, + manualRequired: Bool = false, + snapshot: CAAMEnvironmentSnapshot? = nil) + { + self.operationID = operationID + self.state = state + self.effect = effect + self.previousProfile = previousProfile + self.currentProfile = currentProfile + self.recoveryRequired = recoveryRequired + self.manualRequired = manualRequired + self.snapshot = snapshot + } + + private enum CodingKeys: String, CodingKey { + case operationID = "operation_id" + case state + case effect + case previousProfile = "previous_profile" + case currentProfile = "current_profile" + case recoveryRequired = "recovery_required" + case manualRequired = "manual_required" + case snapshot + } +} + +/// Closed presentation details only. Raw process output never enters control state or UI. +public struct CAAMControlFailure: Error, LocalizedError, Sendable, Equatable { + public enum Reason: String, Sendable { + case unavailable + case timeout + case cancelled + case oversized + case incompatible + case invalidResponse + case rejected + case transport + case stale + case notQualified + case busy + } + + public let reason: Reason + public let effect: CAAMControlEffect + public let code: String? + + public init(reason: Reason, effect: CAAMControlEffect = .knownNoEffect, code: String? = nil) { + self.reason = reason + self.effect = effect + self.code = code + } + + public var errorDescription: String? { + let summary = switch self.reason { + case .unavailable: "CAAM is unavailable." + case .timeout: "The CAAM request timed out." + case .cancelled: "The CAAM request was interrupted." + case .oversized: "The CAAM response exceeded its size limit." + case .incompatible: "The CAAM protocol is unsupported." + case .invalidResponse: "The CAAM response could not be verified." + case .rejected: "CAAM rejected the request." + case .transport: "The CAAM transport failed." + case .stale: "Refresh this environment and request a new plan." + case .notQualified: "Environment mutations require independently qualified CAAM conformance." + case .busy: "Resolve the current CAAM operation first." + } + return self.effect == .unknown + ? summary + " The effect is unknown; look up the existing operation, do not execute again." + : summary + } +} + +struct CAAMControlEnvelope: Codable, Sendable { + let schema: String + let kind: String + let environmentID: String + let protocolVersion: String + let result: Value? + let error: CAAMControlErrorPayload? + + private enum CodingKeys: String, CodingKey { + case schema + case kind + case environmentID = "environment_id" + case protocolVersion = "protocol_version" + case result + case error + } +} + +extension CAAMGatewayOperation { + public var kind: String { + switch self { + case .snapshot: "snapshot" + case .planSwitch: "plan-switch" + case .executeSwitch: "execute-switch" + case .operationStatus: "operation-status" + case .recoverSwitch: "recover-switch" + case .restoreFallback: "restore-fallback" + } + } + + public var operationID: UUID? { + switch self { + case let .executeSwitch(_, _, id, _), let .operationStatus(_, id), + let .recoverSwitch(_, id, _), let .restoreFallback(_, _, _, id, _): id + case .snapshot, .planSwitch: nil + } + } +} diff --git a/Sources/CodexBarCore/Providers/Codex/CAAM/CAAMControlSession.swift b/Sources/CodexBarCore/Providers/Codex/CAAM/CAAMControlSession.swift new file mode 100644 index 0000000000..246f7206a7 --- /dev/null +++ b/Sources/CodexBarCore/Providers/Codex/CAAM/CAAMControlSession.swift @@ -0,0 +1,270 @@ +import Foundation + +public enum CAAMControlPhase: String, Sendable { + case idle + case planning + case awaitingConfirmation + case executing + case resolving + case unknown + case recoveryRequired + case manualRequired + case finished + + public var isBusy: Bool { + self == .planning || self == .executing || self == .resolving + } +} + +public struct CAAMControlConfirmation: Sendable, Equatable, Identifiable { + public enum Kind: String, Sendable { + case switchProfile + case restoreFallback + case recoverSwitch + } + + public let id: UUID + public let kind: Kind + public let revision: String + public let plan: CAAMSwitchPlan? + public let operationID: UUID? +} + +/// Pure execute-once state machine. Configuration identity, not just its ID, owns cached evidence. +public struct CAAMControlSession: Sendable, Equatable { + public let configuration: CAAMEnvironmentConfiguration + public private(set) var snapshot: CAAMEnvironmentSnapshot? + public private(set) var receivedAt: Date? + public private(set) var refreshFailed = false + public private(set) var phase: CAAMControlPhase = .idle + public private(set) var confirmation: CAAMControlConfirmation? + public private(set) var pendingOperationID: UUID? + public private(set) var lastResult: CAAMOperationResult? + public private(set) var failure: CAAMControlFailure? + private var requestedTarget: String? + private var requestedFallback = false + private var expectedCommittedProfile: String? + + public init(configuration: CAAMEnvironmentConfiguration, pendingOperationID: UUID? = nil) { + self.configuration = configuration + self.pendingOperationID = pendingOperationID + if pendingOperationID != nil { self.phase = .unknown } + } + + public var canForget: Bool { + self.pendingOperationID == nil && !self.phase.isBusy + } + + public func cachedSnapshot(now: Date) -> CAAMEnvironmentSnapshot? { + guard let snapshot, let receivedAt, + now.timeIntervalSince(receivedAt) >= 0, + now.timeIntervalSince(receivedAt) <= CAAMEnvironmentContract.maximumCacheAge, + now.timeIntervalSince(snapshot.observedAt) >= -CAAMEnvironmentContract.clockSkewAllowance, + now.timeIntervalSince(snapshot.observedAt) <= CAAMEnvironmentContract.maximumCacheAge + else { return nil } + return snapshot + } + + public func row(now: Date) -> CAAMEnvironmentRowState { + guard let snapshot = self.cachedSnapshot(now: now) else { + if self.failure?.reason == .incompatible { + return CAAMEnvironmentRowState( + id: self.configuration.id, + label: self.configuration.label, + connectionKind: self.configuration.connection.kind, + availability: .incompatible, + message: self.failure?.localizedDescription) + } + return CAAMEnvironmentContract.unavailableRow( + configuration: self.configuration, + message: self.failure?.localizedDescription ?? "Not checked yet.") + } + return CAAMEnvironmentContract.rowState( + configuration: self.configuration, + snapshot: snapshot, + now: now, + refreshFailed: self.refreshFailed || self.phase == .unknown || self.phase.isBusy) + } + + public mutating func observe(_ snapshot: CAAMEnvironmentSnapshot, now: Date) { + self.snapshot = snapshot + self.receivedAt = now + self.refreshFailed = false + self.confirmation = nil + self.failure = nil + // A snapshot alone cannot settle a locally ambiguous operation, even if its journal is empty. + if self.pendingOperationID == nil { + self.pendingOperationID = snapshot.pendingOperation?.operationID + } + if let pendingOperationID = self.pendingOperationID { + if let pending = snapshot.pendingOperation, pending.operationID == pendingOperationID { + self.phase = switch pending.state { + case .recoveryRequired: .recoveryRequired + case .manualRequired: .manualRequired + case .planned, .executing: .unknown + } + } else { + self.phase = .unknown + } + } else { + self.phase = .idle + } + } + + public mutating func failRefresh(_ failure: CAAMControlFailure) { + self.refreshFailed = true + self.failure = failure + self.confirmation = nil + if self.phase == .awaitingConfirmation { self.phase = self.pendingOperationID == nil ? .idle : .unknown } + } + + public mutating func beginPlan(target: String, fallback: Bool, now: Date) throws -> String { + guard self.canForget else { throw CAAMControlFailure(reason: .busy) } + let row = self.row(now: now) + guard let snapshot, (fallback ? row.canRestoreFallback : row.canSwitch), + !fallback || target == snapshot.fallbackProfile, + snapshot.profiles.contains(where: { $0.name == target && $0.eligible && !$0.active }) + else { throw CAAMControlFailure(reason: .stale) } + self.requestedTarget = target + self.requestedFallback = fallback + self.confirmation = nil + self.failure = nil + self.phase = .planning + return snapshot.revision + } + + public mutating func acceptPlan(_ plan: CAAMSwitchPlan, now: Date) throws { + guard self.phase == .planning, let snapshot, let target = self.requestedTarget else { + throw CAAMControlFailure(reason: .stale) + } + try CAAMEnvironmentContract.validatePlan(plan, snapshot: snapshot, target: target, now: now) + self.confirmation = CAAMControlConfirmation( + id: UUID(), + kind: self.requestedFallback ? .restoreFallback : .switchProfile, + revision: snapshot.revision, + plan: plan, + operationID: nil) + self.phase = .awaitingConfirmation + } + + public mutating func failPlan(_ failure: CAAMControlFailure) { + self.phase = .idle + self.confirmation = nil + self.failure = failure + self.refreshFailed = true + } + + public mutating func prepareRecovery(now: Date) throws { + guard !self.phase.isBusy, self.row(now: now).canRecover, + let snapshot, let operationID = self.pendingOperationID, + snapshot.pendingOperation?.operationID == operationID + else { throw CAAMControlFailure(reason: .stale) } + self.confirmation = CAAMControlConfirmation( + id: UUID(), kind: .recoverSwitch, revision: snapshot.revision, plan: nil, operationID: operationID) + self.phase = .awaitingConfirmation + } + + public mutating func cancelConfirmation() { + self.confirmation = nil + self.phase = self.pendingOperationID == nil ? .idle : .unknown + } + + public mutating func confirm( + id: UUID, + mutationsQualified: Bool, + now: Date) throws -> CAAMGatewayOperation + { + guard mutationsQualified else { throw CAAMControlFailure(reason: .notQualified) } + guard let confirmation, confirmation.id == id, self.phase == .awaitingConfirmation, + let snapshot, snapshot.revision == confirmation.revision, + CAAMEnvironmentContract.isCurrent(snapshot, now: now), !self.refreshFailed + else { throw CAAMControlFailure(reason: .stale) } + let operation: CAAMGatewayOperation + switch confirmation.kind { + case .recoverSwitch: + guard self.row(now: now).canRecover, let operationID = confirmation.operationID, + operationID == self.pendingOperationID + else { throw CAAMControlFailure(reason: .stale) } + operation = .recoverSwitch( + environmentID: self.configuration.id, operationID: operationID, expectedRevision: snapshot.revision) + case .switchProfile, .restoreFallback: + let row = self.row(now: now) + guard self.pendingOperationID == nil, let plan = confirmation.plan, + (confirmation.kind == .restoreFallback ? row.canRestoreFallback : row.canSwitch) + else { throw CAAMControlFailure(reason: .stale) } + try CAAMEnvironmentContract.validatePlan(plan, snapshot: snapshot, target: plan.targetProfile, now: now) + let operationID = UUID() + let idempotencyKey = UUID() + if confirmation.kind == .restoreFallback { + guard plan.targetProfile == snapshot.fallbackProfile else { + throw CAAMControlFailure(reason: .stale) + } + operation = .restoreFallback( + environmentID: self.configuration.id, expectedRevision: snapshot.revision, + planDigest: plan.planDigest, operationID: operationID, idempotencyKey: idempotencyKey) + } else { + operation = .executeSwitch( + environmentID: self.configuration.id, planDigest: plan.planDigest, + operationID: operationID, idempotencyKey: idempotencyKey) + } + self.expectedCommittedProfile = plan.targetProfile + } + self.pendingOperationID = operation.operationID + self.confirmation = nil + self.phase = .executing + return operation + } + + public mutating func beginLookup() throws -> CAAMGatewayOperation { + guard !self.phase.isBusy, let operationID = self.pendingOperationID else { + throw CAAMControlFailure(reason: .busy) + } + self.confirmation = nil + self.phase = .resolving + return .operationStatus(environmentID: self.configuration.id, operationID: operationID) + } + + public mutating func acceptResult(_ result: CAAMOperationResult, now: Date) throws { + guard let operationID = self.pendingOperationID else { + throw CAAMControlFailure(reason: .invalidResponse, effect: .unknown) + } + try CAAMEnvironmentContract.validateOperationResult(result, operationID: operationID) + if result.state.isTerminal { + guard let snapshot = result.snapshot, CAAMEnvironmentContract.isCurrent(snapshot, now: now), + result.state != .committed || self.expectedCommittedProfile == nil || + result.currentProfile == self.expectedCommittedProfile + else { throw CAAMControlFailure(reason: .invalidResponse, effect: .unknown) } + self.pendingOperationID = nil + self.expectedCommittedProfile = nil + self.phase = .finished + } else { + self.phase = switch result.state { + case .recoveryRequired: .recoveryRequired + case .manualRequired: .manualRequired + default: .unknown + } + } + if let snapshot = result.snapshot { + self.snapshot = snapshot + self.receivedAt = now + self.refreshFailed = false + } + self.lastResult = result + self.failure = nil + } + + public mutating func failOperation(_ failure: CAAMControlFailure, operation: CAAMGatewayOperation) { + self.failure = failure + self.refreshFailed = true + // No-effect lookup/recovery failures say nothing about the original switch's effect. + if failure.effect == .knownNoEffect, + operation.kind == "execute-switch" || operation.kind == "restore-fallback" + { + self.pendingOperationID = nil + self.expectedCommittedProfile = nil + self.phase = .idle + } else { + self.phase = .unknown + } + } +} diff --git a/Sources/CodexBarCore/Providers/Codex/CAAM/CAAMControlValidation.swift b/Sources/CodexBarCore/Providers/Codex/CAAM/CAAMControlValidation.swift new file mode 100644 index 0000000000..6b758d0fee --- /dev/null +++ b/Sources/CodexBarCore/Providers/Codex/CAAM/CAAMControlValidation.swift @@ -0,0 +1,124 @@ +import Foundation + +extension CAAMEnvironmentContract { + public static let snapshotFreshness: TimeInterval = 60 + public static let maximumCacheAge: TimeInterval = 15 * 60 + public static let clockSkewAllowance: TimeInterval = 5 + + public static func isCurrent(_ snapshot: CAAMEnvironmentSnapshot, now: Date) -> Bool { + let age = now.timeIntervalSince(snapshot.observedAt) + return age.isFinite && age >= -self.clockSkewAllowance && age <= self.snapshotFreshness + } + + static func decodeResponse( + _ type: Value.Type, + data: Data, + environmentID: String, + kind: String) throws -> Value + { + guard data.count <= self.maximumOutputBytes else { + throw CAAMEnvironmentContractError.outputTooLarge + } + guard String(data: data, encoding: .utf8) != nil else { + throw CAAMEnvironmentContractError.invalidEncoding + } + guard let object = try? JSONSerialization.jsonObject(with: data), + !self.containsForbiddenCredentialKey(object) + else { + throw CAAMEnvironmentContractError.invalidEnvelope("The CAAM response contained invalid fields.") + } + let decoder = JSONDecoder() + decoder.dateDecodingStrategy = .iso8601 + guard let envelope = try? decoder.decode(CAAMControlEnvelope.self, from: data) else { + throw CAAMEnvironmentContractError.invalidEnvelope("The CAAM gateway response was malformed.") + } + let version = envelope.protocolVersion.split(separator: ".", omittingEmptySubsequences: false) + guard envelope.schema == self.schema, + envelope.protocolVersion.utf8.count <= 32, + version.first == "1", + version.allSatisfy({ !$0.isEmpty && $0.utf8.allSatisfy { (48...57).contains($0) } }) + else { + throw CAAMControlFailure(reason: .incompatible) + } + guard envelope.environmentID == environmentID, envelope.kind == kind, + (envelope.result == nil) != (envelope.error == nil) + else { + throw CAAMEnvironmentContractError.invalidEnvelope("The CAAM response binding was invalid.") + } + if let error = envelope.error { + guard self.isSafeIdentifier(error.code, maximumLength: 64), + self.isSafeDisplayText(error.message, maximumLength: 240) + else { + throw CAAMEnvironmentContractError.invalidEnvelope("The CAAM gateway error was invalid.") + } + throw CAAMEnvironmentContractError.remoteError( + code: error.code, + message: "CAAM rejected the request.", + effect: error.effect) + } + guard let result = envelope.result else { + throw CAAMEnvironmentContractError.invalidEnvelope("The CAAM gateway omitted its result.") + } + return result + } + + public static func validatePlan( + _ plan: CAAMSwitchPlan, + snapshot: CAAMEnvironmentSnapshot, + target: String, + now: Date) throws + { + guard self.isCurrent(snapshot, now: now), plan.expiresAt > now, + plan.expectedRevision == snapshot.revision, + plan.currentProfile == snapshot.hostDefaultProfile, + plan.targetProfile == target, + snapshot.profiles.contains(where: { $0.name == target && $0.eligible }), + plan.planDigest.utf8.count == 64, + plan.planDigest.utf8.allSatisfy({ (48...57).contains($0) || (97...102).contains($0) }), + plan.affectedProfiles.count <= self.maximumProfileCount, + Set(plan.affectedProfiles).count == plan.affectedProfiles.count, + Set(plan.affectedProfiles).isSubset(of: Set(snapshot.profiles.map(\.name))), + plan.affectedProfiles.contains(target) + else { + throw CAAMControlFailure(reason: .stale) + } + } + + public static func validateOperationResult(_ result: CAAMOperationResult, operationID: UUID) throws { + guard result.operationID == operationID, + [result.previousProfile, result.currentProfile].compactMap(\.self) + .allSatisfy({ self.isSafeIdentifier($0, maximumLength: 64) }) + else { + throw CAAMControlFailure(reason: .invalidResponse, effect: .unknown) + } + if let snapshot = result.snapshot { + try self.validateSnapshot(snapshot) + guard result.currentProfile == snapshot.hostDefaultProfile, + snapshot.pendingOperation == nil || snapshot.pendingOperation?.operationID == operationID + else { + throw CAAMControlFailure(reason: .invalidResponse, effect: .unknown) + } + } + guard result.recoveryRequired == (result.state == .recoveryRequired), + result.manualRequired == (result.state == .manualRequired) + else { + throw CAAMControlFailure(reason: .invalidResponse, effect: .unknown) + } + if result.state.isTerminal { + guard let snapshot = result.snapshot, snapshot.pendingOperation == nil, result.effect != .unknown, + result.state != .committed || result.effect == .knownEffect, + result.state != .rejected || result.effect == .knownNoEffect, + result.state != .rolledBack || result.currentProfile == result.previousProfile + else { + throw CAAMControlFailure(reason: .invalidResponse, effect: .unknown) + } + } + if result.state == .recoveryRequired || result.state == .manualRequired { + let expected: CAAMPendingOperationState = result.state == .recoveryRequired + ? .recoveryRequired : .manualRequired + guard result.snapshot?.pendingOperation?.state == expected else { + throw CAAMControlFailure(reason: .invalidResponse, effect: .unknown) + } + } + } +} diff --git a/Sources/CodexBarCore/Providers/Codex/CAAM/CAAMEnvironmentClient.swift b/Sources/CodexBarCore/Providers/Codex/CAAM/CAAMEnvironmentClient.swift index 530ff23ffc..cefb8e5071 100644 --- a/Sources/CodexBarCore/Providers/Codex/CAAM/CAAMEnvironmentClient.swift +++ b/Sources/CodexBarCore/Providers/Codex/CAAM/CAAMEnvironmentClient.swift @@ -39,6 +39,7 @@ public struct DefaultCAAMEnvironmentCommandRunner: CAAMEnvironmentCommandRunning environment: environment, timeout: command.timeout, maxOutputBytes: command.maximumOutputBytes, + acceptsNonZeroExit: true, label: command.label) } } @@ -220,9 +221,120 @@ public struct CAAMEnvironmentClient: Sendable { } catch { throw CAAMEnvironmentClientError.commandFailed } - return try CAAMEnvironmentContract.decodeSnapshot( + guard result.stdoutWasValidUTF8 else { + throw CAAMEnvironmentContractError.invalidEncoding + } + let snapshot = try CAAMEnvironmentContract.decodeSnapshot( Data(result.stdout.utf8), expectedEnvironmentID: configuration.id) + guard result.exitCode == 0 else { throw CAAMEnvironmentClientError.commandFailed } + return snapshot + } + + public func fetchPlan( + for configuration: CAAMEnvironmentConfiguration, + profile: String, + expectedRevision: String) async throws -> CAAMSwitchPlan + { + try await self.request( + CAAMSwitchPlan.self, + configuration: configuration, + operation: .planSwitch( + environmentID: configuration.id, + profile: profile, + expectedRevision: expectedRevision)) + } + + public func perform( + _ operation: CAAMGatewayOperation, + for configuration: CAAMEnvironmentConfiguration) async throws -> CAAMOperationResult + { + guard let operationID = operation.operationID else { + throw CAAMControlFailure(reason: .invalidResponse) + } + let result = try await self.request(CAAMOperationResult.self, configuration: configuration, operation: operation) + do { + try CAAMEnvironmentContract.validateOperationResult(result, operationID: operationID) + } catch { + throw CAAMControlFailure(reason: .invalidResponse, effect: .unknown) + } + return result + } + + private func request( + _ type: Value.Type, + configuration: CAAMEnvironmentConfiguration, + operation: CAAMGatewayOperation) async throws -> Value + { + let command: CAAMEnvironmentCommand + do { + try Task.checkCancellation() + command = try self.command(for: configuration, operation: operation) + } catch { + throw Self.failure(for: error, possibleEffect: .knownNoEffect) + } + let possibleEffect: CAAMControlEffect = operation.mayMutateCredentials ? .unknown : .knownNoEffect + let response: SubprocessResult + do { + response = try await self.runner.run(command: command, environment: self.environment) + } catch { + throw Self.failure(for: error, possibleEffect: possibleEffect) + } + do { + guard response.stdoutWasValidUTF8 else { + throw CAAMEnvironmentContractError.invalidEncoding + } + let result = try CAAMEnvironmentContract.decodeResponse( + type, + data: Data(response.stdout.utf8), + environmentID: configuration.id, + kind: operation.kind) + guard response.exitCode == 0 else { + throw CAAMControlFailure(reason: .transport, effect: possibleEffect) + } + return result + } catch { + throw Self.failure(for: error, possibleEffect: possibleEffect) + } + } + + public static func failure(for error: any Error, possibleEffect: CAAMControlEffect) -> CAAMControlFailure { + if let error = error as? CAAMControlFailure { + return CAAMControlFailure(reason: error.reason, effect: possibleEffect, code: error.code) + } + if error is CancellationError { + return CAAMControlFailure(reason: .cancelled, effect: possibleEffect) + } + if let error = error as? CAAMEnvironmentContractError { + switch error { + case let .remoteError(code, _, effect): + return CAAMControlFailure(reason: .rejected, effect: effect, code: code) + case .outputTooLarge: + return CAAMControlFailure(reason: .oversized, effect: possibleEffect) + case .invalidConfiguration: + return CAAMControlFailure(reason: .invalidResponse) + case .invalidEncoding, .invalidEnvelope: + return CAAMControlFailure(reason: .invalidResponse, effect: possibleEffect) + } + } + if let error = error as? SubprocessRunnerError { + switch error { + case .binaryNotFound, .launchFailed: return CAAMControlFailure(reason: .unavailable) + case .timedOut: return CAAMControlFailure(reason: .timeout, effect: possibleEffect) + case .outputTooLarge: return CAAMControlFailure(reason: .oversized, effect: possibleEffect) + case .nonZeroExit: return CAAMControlFailure(reason: .transport, effect: possibleEffect) + } + } + if let error = error as? CAAMEnvironmentClientError { + switch error { + case .binaryUnavailable: return CAAMControlFailure(reason: .unavailable) + case .timedOut: return CAAMControlFailure(reason: .timeout, effect: possibleEffect) + case .responseTooLarge: return CAAMControlFailure(reason: .oversized, effect: possibleEffect) + case .cancelled: return CAAMControlFailure(reason: .cancelled, effect: possibleEffect) + case .commandFailed: return CAAMControlFailure(reason: .transport, effect: possibleEffect) + } + } + return CAAMControlFailure(reason: .transport, effect: possibleEffect) } public func command( @@ -276,6 +388,7 @@ public struct CAAMEnvironmentClient: Sendable { } let path = environment["PATH"] ?? "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin" return path.split(separator: ":") + .filter { $0.hasPrefix("/") } .map { String($0) + "/" + Self.defaultGatewayName } .first { fileManager.isExecutableFile(atPath: $0) } } diff --git a/Sources/CodexBarCore/Providers/Codex/CAAM/CAAMEnvironmentModels.swift b/Sources/CodexBarCore/Providers/Codex/CAAM/CAAMEnvironmentModels.swift index d5d2202ac9..ba5fe209d8 100644 --- a/Sources/CodexBarCore/Providers/Codex/CAAM/CAAMEnvironmentModels.swift +++ b/Sources/CodexBarCore/Providers/Codex/CAAM/CAAMEnvironmentModels.swift @@ -300,6 +300,7 @@ public enum CAAMEnvironmentAvailability: Sendable, Equatable { case incompatible case recoveryRequired case unavailable + case stale } public struct CAAMEnvironmentRowState: Sendable, Equatable, Identifiable { diff --git a/Sources/CodexBarCore/Providers/Codex/CAAM/CAAMEnvironmentValidation.swift b/Sources/CodexBarCore/Providers/Codex/CAAM/CAAMEnvironmentValidation.swift index e758c3bdfb..1feecd70db 100644 --- a/Sources/CodexBarCore/Providers/Codex/CAAM/CAAMEnvironmentValidation.swift +++ b/Sources/CodexBarCore/Providers/Codex/CAAM/CAAMEnvironmentValidation.swift @@ -80,87 +80,50 @@ public enum CAAMEnvironmentContract { _ data: Data, expectedEnvironmentID: String) throws -> CAAMEnvironmentSnapshot { - guard data.count <= self.maximumOutputBytes else { - throw CAAMEnvironmentContractError.outputTooLarge - } - guard String(data: data, encoding: .utf8) != nil else { - throw CAAMEnvironmentContractError.invalidEncoding - } - guard let rawObject = try? JSONSerialization.jsonObject(with: data), - !self.containsForbiddenCredentialKey(rawObject) - else { - throw CAAMEnvironmentContractError.invalidEnvelope( - "The CAAM gateway response contained a credential-bearing field.") - } - - let decoder = JSONDecoder() - decoder.dateDecodingStrategy = .iso8601 - let envelope: CAAMSnapshotEnvelope - do { - envelope = try decoder.decode(CAAMSnapshotEnvelope.self, from: data) - } catch { - throw CAAMEnvironmentContractError.invalidEnvelope("The CAAM gateway response was malformed.") - } - - guard envelope.schema == self.schema, envelope.kind == "snapshot" else { - throw CAAMEnvironmentContractError.invalidEnvelope("The CAAM gateway response used an unsupported schema.") - } - guard envelope.environmentID == expectedEnvironmentID else { - throw CAAMEnvironmentContractError.invalidEnvelope( - "The CAAM gateway response identified another environment.") - } - guard envelope.protocolVersion.count <= 32, - self.isASCIIPrintable(envelope.protocolVersion), - envelope.protocolVersion.split(separator: ".").first == "1" - else { - throw CAAMEnvironmentContractError.invalidEnvelope("The CAAM gateway protocol version is unsupported.") - } - guard (envelope.result == nil) != (envelope.error == nil) else { - throw CAAMEnvironmentContractError.invalidEnvelope( - "The CAAM gateway response must contain exactly one result or error.") - } - if let error = envelope.error { - guard self.isSafeIdentifier(error.code, maximumLength: 64), - self.isSafeDisplayText(error.message, maximumLength: 240) - else { - throw CAAMEnvironmentContractError.invalidEnvelope("The CAAM gateway error was invalid.") - } - throw CAAMEnvironmentContractError.remoteError( - code: error.code, - message: error.message, - effect: error.effect) - } - guard let snapshot = envelope.result else { - throw CAAMEnvironmentContractError.invalidEnvelope("The CAAM gateway response omitted its result.") - } + let snapshot = try self.decodeResponse( + CAAMEnvironmentSnapshot.self, + data: data, + environmentID: expectedEnvironmentID, + kind: "snapshot") try self.validateSnapshot(snapshot) return snapshot } public static func rowState( configuration: CAAMEnvironmentConfiguration, - snapshot: CAAMEnvironmentSnapshot) -> CAAMEnvironmentRowState + snapshot: CAAMEnvironmentSnapshot, + now: Date = Date(), + refreshFailed: Bool = false) -> CAAMEnvironmentRowState { + let current = self.isCurrent(snapshot, now: now) && !refreshFailed let capabilities = snapshot.supportedCapabilities let hasAllSwitchCapabilities = capabilities.isSuperset(of: [ + .snapshot, .planSwitch, .executeSwitch, .operationStatus, + .recoverSwitch, ]) let hasPending = snapshot.pendingOperation != nil let currentProfile = snapshot.profiles.first(where: { $0.name == snapshot.hostDefaultProfile }) - let accountLabel = currentProfile?.identity.flatMap(Self.accountLabel) - let availability: CAAMEnvironmentAvailability = if snapshot.pendingOperation?.state == .manualRequired || + let accountLabel = currentProfile?.identity.flatMap { identity in + identity.provider == "codex" ? Self.accountLabel(identity) : nil + } + let availability: CAAMEnvironmentAvailability = if !capabilities.contains(.snapshot) { + .incompatible + } else if !current { + .stale + } else if snapshot.pendingOperation?.state == .manualRequired || snapshot.pendingOperation?.state == .recoveryRequired { .recoveryRequired } else if snapshot.reachability == .degraded || !snapshot.warnings.isEmpty { .degraded - } else if !capabilities.contains(.snapshot) { - .incompatible } else { .ready } + let canMutate = current && availability == .ready && hasAllSwitchCapabilities && !hasPending + let eligibleTargets = snapshot.profiles.filter { $0.eligible && $0.name != snapshot.hostDefaultProfile } return CAAMEnvironmentRowState( id: configuration.id, @@ -174,11 +137,13 @@ public enum CAAMEnvironmentContract { reloadRequired: snapshot.runtime.reloadRequired, caamVersion: snapshot.caamVersion, observedAt: snapshot.observedAt, - message: snapshot.warnings.first, - canSwitch: hasAllSwitchCapabilities && !hasPending, - canRecover: capabilities.contains(.recoverSwitch) && hasPending, - canRestoreFallback: capabilities.contains(.restoreFallback) && - snapshot.fallbackProfile != nil && !hasPending) + message: current ? snapshot.warnings.first : "Cached state is stale; refresh before changing environments.", + canSwitch: canMutate && !eligibleTargets.isEmpty, + canRecover: current && snapshot.reachability == .reachable && + capabilities.isSuperset(of: [.snapshot, .recoverSwitch, .operationStatus]) && + snapshot.pendingOperation?.state == .recoveryRequired, + canRestoreFallback: canMutate && capabilities.contains(.restoreFallback) && + eligibleTargets.contains { $0.name == snapshot.fallbackProfile }) } public static func unavailableRow( @@ -215,7 +180,7 @@ public enum CAAMEnvironmentContract { } } - private static func validateSnapshot(_ snapshot: CAAMEnvironmentSnapshot) throws { + static func validateSnapshot(_ snapshot: CAAMEnvironmentSnapshot) throws { guard self.isSafeRevision(snapshot.revision), self.isSafeVersion(snapshot.caamVersion), snapshot.capabilities.count <= CAAMControlCapability.allCases.count + 16, @@ -287,9 +252,9 @@ public enum CAAMEnvironmentContract { return self.isASCIIPrintable(value) } - private static func isSafeDisplayText(_ value: String, maximumLength: Int) -> Bool { + static func isSafeDisplayText(_ value: String, maximumLength: Int) -> Bool { let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines) - guard !trimmed.isEmpty, trimmed == value, value.count <= maximumLength else { return false } + guard !trimmed.isEmpty, trimmed == value, value.unicodeScalars.count <= maximumLength else { return false } return value.unicodeScalars.allSatisfy { !CharacterSet.controlCharacters.contains($0) } } @@ -315,13 +280,19 @@ public enum CAAMEnvironmentContract { } } - private static func containsForbiddenCredentialKey(_ value: Any) -> Bool { + static func containsForbiddenCredentialKey(_ value: Any, depth: Int = 0) -> Bool { + guard depth <= 32 else { return true } let forbiddenKeys = Set([ "accesstoken", "apikey", "auth", "authorization", "authjson", + "authfiledigest", + "authfilehash", + "command", + "commandtext", + "shellfragment", "authpath", "bearer", "cookie", @@ -339,11 +310,11 @@ public enum CAAMEnvironmentContract { if let object = value as? [String: Any] { return object.contains { key, child in let normalizedKey = key.lowercased().filter { $0.isASCII && ($0.isLetter || $0.isNumber) } - return forbiddenKeys.contains(normalizedKey) || self.containsForbiddenCredentialKey(child) + return forbiddenKeys.contains(normalizedKey) || self.containsForbiddenCredentialKey(child, depth: depth + 1) } } if let array = value as? [Any] { - return array.contains(where: self.containsForbiddenCredentialKey) + return array.contains { self.containsForbiddenCredentialKey($0, depth: depth + 1) } } return false } diff --git a/Sources/CodexBarCore/Providers/Codex/CAAM/CAAMOperationReceiptStore.swift b/Sources/CodexBarCore/Providers/Codex/CAAM/CAAMOperationReceiptStore.swift new file mode 100644 index 0000000000..e9c86ffdcb --- /dev/null +++ b/Sources/CodexBarCore/Providers/Codex/CAAM/CAAMOperationReceiptStore.swift @@ -0,0 +1,66 @@ +import Foundation + +public struct CAAMOperationReceipt: Codable, Sendable, Equatable { + public let configuration: CAAMEnvironmentConfiguration + public let operationID: UUID + + public init(configuration: CAAMEnvironmentConfiguration, operationID: UUID) { + self.configuration = configuration + self.operationID = operationID + } +} + +public protocol CAAMOperationReceiptStoring: Sendable { + func load() async throws -> [CAAMOperationReceipt] + func save(_ receipts: [CAAMOperationReceipt]) async throws +} + +public actor CAAMMemoryReceiptStore: CAAMOperationReceiptStoring { + private var receipts: [CAAMOperationReceipt] + + public init(receipts: [CAAMOperationReceipt] = []) { + self.receipts = receipts + } + + public func load() -> [CAAMOperationReceipt] { self.receipts } + + public func save(_ receipts: [CAAMOperationReceipt]) { + self.receipts = receipts + } +} + +/// Actor-isolated bounded file IO, never on the UI actor. Stores selectors and operation IDs, not responses. +public actor CAAMFileReceiptStore: CAAMOperationReceiptStoring { + private let url: URL + + public init(url: URL) { + self.url = url + } + + public func load() throws -> [CAAMOperationReceipt] { + guard FileManager.default.fileExists(atPath: self.url.path) else { return [] } + let file = try FileHandle(forReadingFrom: self.url) + defer { try? file.close() } + let data = try file.read(upToCount: CAAMEnvironmentContract.maximumOutputBytes + 1) ?? Data() + guard data.count <= CAAMEnvironmentContract.maximumOutputBytes else { + throw CAAMControlFailure(reason: .oversized) + } + let receipts = try JSONDecoder().decode([CAAMOperationReceipt].self, from: data) + _ = try CAAMEnvironmentContract.validateConfigurations(receipts.map(\.configuration)) + return receipts + } + + public func save(_ receipts: [CAAMOperationReceipt]) throws { + _ = try CAAMEnvironmentContract.validateConfigurations(receipts.map(\.configuration)) + let data = try JSONEncoder().encode(receipts) + guard data.count <= CAAMEnvironmentContract.maximumOutputBytes else { + throw CAAMControlFailure(reason: .oversized) + } + try FileManager.default.createDirectory( + at: self.url.deletingLastPathComponent(), + withIntermediateDirectories: true, + attributes: [.posixPermissions: 0o700]) + try data.write(to: self.url, options: .atomic) + try FileManager.default.setAttributes([.posixPermissions: 0o600], ofItemAtPath: self.url.path) + } +} diff --git a/TestsLinux/CAAMControlFixtures.swift b/TestsLinux/CAAMControlFixtures.swift new file mode 100644 index 0000000000..3efe7f2599 --- /dev/null +++ b/TestsLinux/CAAMControlFixtures.swift @@ -0,0 +1,93 @@ +import Foundation +@testable import CodexBarCore + +enum CAAMControlFixtures { + static let now = Date(timeIntervalSince1970: 1_700_000_000) + static let configuration = CAAMEnvironmentConfiguration(id: "fixture", label: "Fixture", connection: .local()) + static let operationID = UUID(uuid: (0, 0, 0, 0, 0, 0, 64, 0, 128, 0, 0, 0, 0, 0, 0, 1)) + + static func snapshot( + revision: String = "1", + date: Date = Self.now, + capabilities: [String] = CAAMControlCapability.allCases.map(\.rawValue), + reachability: CAAMEnvironmentReachability = .reachable, + active: String = "primary", + pending: CAAMPendingOperation? = nil, + warnings: [String] = [], + provider: String = "codex", + stableID: String = "fixture-account") -> CAAMEnvironmentSnapshot + { + CAAMEnvironmentSnapshot( + revision: revision, observedAt: date, caamVersion: "fixture-1", capabilities: capabilities, + reachability: reachability, hostDefaultProfile: active, fallbackProfile: "secondary", + profiles: ["primary", "secondary"].map { name in + CAAMEnvironmentProfile( + name: name, active: name == active, system: false, eligible: true, health: .healthy, + identity: CAAMProfileIdentity(provider: provider, stableID: stableID, displayEmail: "f***@example.invalid")) + }, + runtime: CAAMRuntimeSnapshot(state: .unknown, reloadRequired: false), + pendingOperation: pending, + warnings: warnings) + } + + static func plan( + revision: String = "1", + target: String = "secondary", + expiresAt: Date = Self.now.addingTimeInterval(30)) -> CAAMSwitchPlan + { + CAAMSwitchPlan( + planDigest: String(repeating: "a", count: 64), expectedRevision: revision, + currentProfile: "primary", targetProfile: target, affectedProfiles: ["primary", "secondary"], + reloadRequired: true, expiresAt: expiresAt) + } + + static func session() -> CAAMControlSession { + var session = CAAMControlSession(configuration: Self.configuration) + session.observe(Self.snapshot(), now: Self.now) + return session + } + + static func envelope( + _ result: Value, + kind: String, + environmentID: String = Self.configuration.id, + version: String = "1.0") throws -> String + { + let envelope = CAAMControlEnvelope( + schema: CAAMEnvironmentContract.schema, kind: kind, environmentID: environmentID, + protocolVersion: version, result: result, error: nil) + let encoder = JSONEncoder() + encoder.dateEncodingStrategy = .iso8601 + return String(decoding: try encoder.encode(envelope), as: UTF8.self) + } + + static func committed(operationID: UUID = Self.operationID) -> CAAMOperationResult { + CAAMOperationResult( + operationID: operationID, state: .committed, effect: .knownEffect, + previousProfile: "primary", currentProfile: "secondary", + snapshot: Self.snapshot(revision: "2", active: "secondary")) + } +} + +actor CAAMControlFixtureRunner: CAAMEnvironmentCommandRunning { + enum Step: Sendable { + case result(SubprocessResult) + case failure(SubprocessRunnerError) + case cancelled + } + + private var steps: [Step] + private(set) var commands: [CAAMEnvironmentCommand] = [] + + init(_ steps: [Step]) { self.steps = steps } + + func run(command: CAAMEnvironmentCommand, environment _: [String: String]) throws -> SubprocessResult { + self.commands.append(command) + guard !self.steps.isEmpty else { throw SubprocessRunnerError.launchFailed("fixture exhausted") } + switch self.steps.removeFirst() { + case let .result(result): return result + case let .failure(error): throw error + case .cancelled: throw CancellationError() + } + } +} diff --git a/TestsLinux/CAAMControlSessionLinuxTests.swift b/TestsLinux/CAAMControlSessionLinuxTests.swift new file mode 100644 index 0000000000..e05d20dd91 --- /dev/null +++ b/TestsLinux/CAAMControlSessionLinuxTests.swift @@ -0,0 +1,166 @@ +import Foundation +import Testing +@testable import CodexBarCore + +struct CAAMControlSessionLinuxTests { + @Test + func `freshness and complete capabilities gate every mutation`() { + let fixture = CAAMControlFixtures.self + let snapshot = fixture.snapshot() + let ready = CAAMEnvironmentContract.rowState( + configuration: fixture.configuration, snapshot: snapshot, now: fixture.now) + #expect(ready.canSwitch) + #expect(ready.canRestoreFallback) + for age in [61.0, -6.0] { + let row = CAAMEnvironmentContract.rowState( + configuration: fixture.configuration, snapshot: snapshot, now: fixture.now.addingTimeInterval(age)) + #expect(row.availability == .stale) + #expect(!row.canSwitch && !row.canRecover && !row.canRestoreFallback) + } + for capability in ["snapshot", "plan_switch", "execute_switch", "operation_status", "recover_switch"] { + let snapshot = fixture.snapshot(capabilities: CAAMControlCapability.allCases.map(\.rawValue) + .filter { $0 != capability } + ["future_switch"]) + let row = CAAMEnvironmentContract.rowState( + configuration: fixture.configuration, snapshot: snapshot, now: fixture.now) + #expect(!row.canSwitch && !row.canRestoreFallback) + } + let degraded = CAAMEnvironmentContract.rowState( + configuration: fixture.configuration, snapshot: fixture.snapshot(reachability: .degraded), now: fixture.now) + #expect(!degraded.canSwitch && !degraded.canRestoreFallback) + } + + @Test + func `confirmation is single use and qualification is independent of capability`() throws { + var session = CAAMControlFixtures.session() + _ = try session.beginPlan(target: "secondary", fallback: false, now: CAAMControlFixtures.now) + try session.acceptPlan(CAAMControlFixtures.plan(), now: CAAMControlFixtures.now) + let confirmation = try #require(session.confirmation) + #expect(throws: CAAMControlFailure.self) { + try session.confirm(id: confirmation.id, mutationsQualified: false, now: CAAMControlFixtures.now) + } + let operation = try session.confirm( + id: confirmation.id, mutationsQualified: true, now: CAAMControlFixtures.now) + #expect(operation.kind == "execute-switch") + #expect(session.pendingOperationID == operation.operationID) + #expect(session.phase == .executing) + #expect(session.confirmation == nil) + #expect(throws: CAAMControlFailure.self) { + try session.confirm(id: confirmation.id, mutationsQualified: true, now: CAAMControlFixtures.now) + } + } + + @Test + func `expired plans and changed revisions invalidate confirmation`() throws { + var session = CAAMControlFixtures.session() + _ = try session.beginPlan(target: "secondary", fallback: false, now: CAAMControlFixtures.now) + try session.acceptPlan(CAAMControlFixtures.plan(), now: CAAMControlFixtures.now) + let confirmation = try #require(session.confirmation) + #expect(throws: CAAMControlFailure.self) { + try session.confirm( + id: confirmation.id, mutationsQualified: true, now: CAAMControlFixtures.now.addingTimeInterval(31)) + } + session.observe(CAAMControlFixtures.snapshot(revision: "2"), now: CAAMControlFixtures.now) + #expect(session.confirmation == nil) + #expect(throws: CAAMControlFailure.self) { + try session.confirm(id: confirmation.id, mutationsQualified: true, now: CAAMControlFixtures.now) + } + } + + @Test + func `ambiguous execution survives snapshot refresh and failed lookup`() throws { + var session = CAAMControlFixtures.session() + _ = try session.beginPlan(target: "secondary", fallback: false, now: CAAMControlFixtures.now) + try session.acceptPlan(CAAMControlFixtures.plan(), now: CAAMControlFixtures.now) + let confirmation = try #require(session.confirmation) + let execute = try session.confirm( + id: confirmation.id, mutationsQualified: true, now: CAAMControlFixtures.now) + session.failOperation(CAAMControlFailure(reason: .timeout, effect: .unknown), operation: execute) + session.observe(CAAMControlFixtures.snapshot(), now: CAAMControlFixtures.now) + #expect(session.phase == .unknown) + #expect(session.pendingOperationID == execute.operationID) + #expect(!session.canForget) + #expect(!session.row(now: CAAMControlFixtures.now).canSwitch) + let lookup = try session.beginLookup() + #expect(lookup.kind == "operation-status") + #expect(lookup.operationID == execute.operationID) + session.failOperation(CAAMControlFailure(reason: .rejected), operation: lookup) + #expect(session.pendingOperationID == execute.operationID) + #expect(session.phase == .unknown) + let result = CAAMControlFixtures.committed(operationID: try #require(execute.operationID)) + _ = try session.beginLookup() + try session.acceptResult(result, now: CAAMControlFixtures.now) + #expect(session.phase == .finished) + #expect(session.pendingOperationID == nil) + #expect(session.snapshot?.hostDefaultProfile == "secondary") + } + + @Test + func `fallback and recovery remain distinct confirmed operations`() throws { + var fallback = CAAMControlFixtures.session() + _ = try fallback.beginPlan(target: "secondary", fallback: true, now: CAAMControlFixtures.now) + try fallback.acceptPlan(CAAMControlFixtures.plan(), now: CAAMControlFixtures.now) + let confirmation = try #require(fallback.confirmation) + let operation = try fallback.confirm( + id: confirmation.id, mutationsQualified: true, now: CAAMControlFixtures.now) + #expect(operation.kind == "restore-fallback") + #expect(try operation.gatewayArguments().contains("--expected-revision")) + + var recovery = CAAMControlFixtures.session() + recovery.observe(CAAMControlFixtures.snapshot(pending: CAAMPendingOperation( + operationID: CAAMControlFixtures.operationID, state: .recoveryRequired)), now: CAAMControlFixtures.now) + try recovery.prepareRecovery(now: CAAMControlFixtures.now) + let recoveryConfirmation = try #require(recovery.confirmation) + let recover = try recovery.confirm( + id: recoveryConfirmation.id, mutationsQualified: true, now: CAAMControlFixtures.now) + #expect(recover.kind == "recover-switch") + #expect(recover.operationID == CAAMControlFixtures.operationID) + recovery.failOperation(CAAMControlFailure(reason: .rejected), operation: recover) + #expect(recovery.pendingOperationID == CAAMControlFixtures.operationID) + } + + @Test + func `manual intervention cannot be silently recovered or replaced by fallback`() { + var session = CAAMControlFixtures.session() + session.observe(CAAMControlFixtures.snapshot(pending: CAAMPendingOperation( + operationID: CAAMControlFixtures.operationID, state: .manualRequired)), now: CAAMControlFixtures.now) + let row = session.row(now: CAAMControlFixtures.now) + #expect(session.phase == .manualRequired) + #expect(!row.canSwitch && !row.canRecover && !row.canRestoreFallback) + #expect(throws: CAAMControlFailure.self) { try session.prepareRecovery(now: CAAMControlFixtures.now) } + } + + @Test + func `stale while revalidate retains bounded evidence without mutation authority`() { + var session = CAAMControlFixtures.session() + session.failRefresh(CAAMControlFailure(reason: .timeout)) + let row = session.row(now: CAAMControlFixtures.now.addingTimeInterval(10)) + #expect(row.availability == .stale) + #expect(row.currentProfile == "primary") + #expect(row.observedAt == CAAMControlFixtures.now) + #expect(!row.canSwitch) + #expect(session.cachedSnapshot(now: CAAMControlFixtures.now.addingTimeInterval(901)) == nil) + #expect(session.row(now: CAAMControlFixtures.now.addingTimeInterval(901)).availability == .unavailable) + } + + @Test + func `terminal result requires exact identity fresh postcondition and truthful rollback`() throws { + let fixture = CAAMControlFixtures.self + #expect(throws: CAAMControlFailure.self) { + try CAAMEnvironmentContract.validateOperationResult(fixture.committed(), operationID: UUID()) + } + let missingSnapshot = CAAMOperationResult( + operationID: fixture.operationID, state: .committed, effect: .knownEffect) + #expect(throws: CAAMControlFailure.self) { + try CAAMEnvironmentContract.validateOperationResult(missingSnapshot, operationID: fixture.operationID) + } + let rollback = CAAMOperationResult( + operationID: fixture.operationID, state: .rolledBack, effect: .knownEffect, + previousProfile: "primary", currentProfile: "primary", snapshot: fixture.snapshot()) + try CAAMEnvironmentContract.validateOperationResult(rollback, operationID: fixture.operationID) + var session = CAAMControlSession(configuration: fixture.configuration, pendingOperationID: fixture.operationID) + #expect(throws: CAAMControlFailure.self) { + try session.acceptResult(fixture.committed(), now: fixture.now.addingTimeInterval(61)) + } + #expect(session.pendingOperationID == fixture.operationID) + } +} diff --git a/TestsLinux/CAAMControlTransportLinuxTests.swift b/TestsLinux/CAAMControlTransportLinuxTests.swift new file mode 100644 index 0000000000..a4f7337eef --- /dev/null +++ b/TestsLinux/CAAMControlTransportLinuxTests.swift @@ -0,0 +1,149 @@ +import Foundation +import Testing +@testable import CodexBarCore + +struct CAAMControlTransportLinuxTests { + @Test + func `relative PATH entries cannot choose a gateway from the working directory`() { + #expect(CAAMEnvironmentClient.resolveLocalGateway(environment: ["PATH": ".:relative/bin"]) == nil) + } + + private func client(_ runner: CAAMControlFixtureRunner) -> CAAMEnvironmentClient { + CAAMEnvironmentClient(runner: runner, environment: [:], resolveLocalGateway: { _ in "/fixture/caam-codexbar" }) + } + + private var operation: CAAMGatewayOperation { + .executeSwitch( + environmentID: CAAMControlFixtures.configuration.id, + planDigest: String(repeating: "a", count: 64), + operationID: CAAMControlFixtures.operationID, + idempotencyKey: CAAMControlFixtures.operationID) + } + + @Test + func `typed plan is decoded from the exact read only operation`() async throws { + let stdout = try CAAMControlFixtures.envelope(CAAMControlFixtures.plan(), kind: "plan-switch") + let runner = CAAMControlFixtureRunner([.result(SubprocessResult(stdout: stdout, stderr: ""))]) + let plan = try await self.client(runner).fetchPlan( + for: CAAMControlFixtures.configuration, profile: "secondary", expectedRevision: "1") + #expect(plan == CAAMControlFixtures.plan()) + let commands = await runner.commands + #expect(commands.count == 1) + #expect(commands.first?.arguments == [ + "plan-switch", "--environment-id", "fixture", "--profile", "secondary", "--expected-revision", "1", + ]) + } + + @Test + func `nonzero error stdout retains typed effect but never diagnostic text`() async throws { + let stdout = #""" + {"schema":"caam.codexbar-control/v1","kind":"execute-switch","environment_id":"fixture", + "protocol_version":"1.0","result":null, + "error":{"code":"stale_revision","message":"private fixture diagnostic","effect":"known_no_effect"}} + """# + let runner = CAAMControlFixtureRunner([.result(SubprocessResult( + stdout: stdout, stderr: "private stderr fixture", exitCode: 2))]) + do { + _ = try await self.client(runner).perform(self.operation, for: CAAMControlFixtures.configuration) + Issue.record("Expected a typed rejection") + } catch let error as CAAMControlFailure { + #expect(error.reason == .rejected) + #expect(error.effect == .knownNoEffect) + #expect(error.code == "stale_revision") + #expect(!error.localizedDescription.contains("private")) + } + } + + @Test + func `nonzero success envelope and invalid UTF8 cannot confirm a mutation`() async throws { + let stdout = try CAAMControlFixtures.envelope(CAAMControlFixtures.committed(), kind: "execute-switch") + for response in [ + SubprocessResult(stdout: stdout, stderr: "", exitCode: 2), + SubprocessResult(stdout: stdout, stderr: "", stdoutWasValidUTF8: false), + SubprocessResult(stdout: String(repeating: " ", count: 65537), stderr: ""), + ] { + let runner = CAAMControlFixtureRunner([.result(response)]) + do { + _ = try await self.client(runner).perform(self.operation, for: CAAMControlFixtures.configuration) + Issue.record("Expected ambiguous failure") + } catch let error as CAAMControlFailure { + #expect(error.effect == .unknown) + } + } + } + + @Test + func `transport failures distinguish no launch from potentially changed credentials`() async throws { + let cases: [(CAAMControlFixtureRunner.Step, CAAMControlEffect)] = [ + (.failure(.binaryNotFound("fixture")), .knownNoEffect), + (.failure(.launchFailed("fixture")), .knownNoEffect), + (.failure(.timedOut("fixture")), .unknown), + (.failure(.outputTooLarge("fixture")), .unknown), + (.failure(.nonZeroExit(code: 1, stderr: "private fixture")), .unknown), + (.cancelled, .unknown), + ] + for (step, effect) in cases { + let runner = CAAMControlFixtureRunner([step]) + do { + _ = try await self.client(runner).perform(self.operation, for: CAAMControlFixtures.configuration) + Issue.record("Expected a transport failure") + } catch let error as CAAMControlFailure { + #expect(error.effect == effect) + #expect(!error.localizedDescription.contains("private")) + } + #expect(await runner.commands.count == 1) + } + } + + @Test + func `cancellation before launch records no effect and invokes no runner`() async { + let runner = CAAMControlFixtureRunner([]) + let client = self.client(runner) + let operation = self.operation + let task = Task { + withUnsafeCurrentTask { $0?.cancel() } + return try await client.perform(operation, for: CAAMControlFixtures.configuration) + } + switch await task.result { + case .success: Issue.record("Expected pre-launch cancellation") + case let .failure(error): + #expect((error as? CAAMControlFailure)?.effect == .knownNoEffect) + #expect((error as? CAAMControlFailure)?.reason == .cancelled) + } + #expect(await runner.commands.isEmpty) + } + + @Test + func `mismatched bindings and unsupported protocol never become success`() async throws { + let fixture = CAAMControlFixtures.self + let responses = [ + try fixture.envelope(fixture.committed(), kind: "operation-status"), + try fixture.envelope(fixture.committed(), kind: "execute-switch", environmentID: "other"), + try fixture.envelope(fixture.committed(), kind: "execute-switch", version: "2.0"), + try fixture.envelope(fixture.committed(), kind: "execute-switch", version: "1.invalid"), + ] + for stdout in responses { + let runner = CAAMControlFixtureRunner([.result(SubprocessResult(stdout: stdout, stderr: ""))]) + do { + _ = try await self.client(runner).perform(self.operation, for: fixture.configuration) + Issue.record("Expected bound response rejection") + } catch let error as CAAMControlFailure { + #expect(error.effect == .unknown) + } + } + } + + @Test + func `credential and command fields are recursively rejected before typed decoding`() throws { + let fixture = CAAMControlFixtures.self + let original = try fixture.envelope(fixture.snapshot(), kind: "snapshot") + for key in ["refresh_token", "auth_file_digest", "shellFragment", "commandText"] { + var object = try #require(JSONSerialization.jsonObject(with: Data(original.utf8)) as? [String: Any]) + object["future_metadata"] = [key: "synthetic fixture"] + let data = try JSONSerialization.data(withJSONObject: object) + #expect(throws: CAAMEnvironmentContractError.self) { + try CAAMEnvironmentContract.decodeSnapshot(data, expectedEnvironmentID: fixture.configuration.id) + } + } + } +} diff --git a/TestsLinux/CAAMEnvironmentContractLinuxTests.swift b/TestsLinux/CAAMEnvironmentContractLinuxTests.swift index 6ea132d28c..831a52da79 100644 --- a/TestsLinux/CAAMEnvironmentContractLinuxTests.swift +++ b/TestsLinux/CAAMEnvironmentContractLinuxTests.swift @@ -49,7 +49,8 @@ struct CAAMEnvironmentContractLinuxTests { Data(Self.snapshotJSON.utf8), expectedEnvironmentID: "laptop") let configuration = CAAMEnvironmentConfiguration(id: "laptop", label: "Laptop", connection: .local()) - let row = CAAMEnvironmentContract.rowState(configuration: configuration, snapshot: snapshot) + let row = CAAMEnvironmentContract.rowState( + configuration: configuration, snapshot: snapshot, now: snapshot.observedAt) #expect(snapshot.hostDefaultProfile == "primary") #expect(snapshot.unknownCapabilities == ["future_capability"]) @@ -146,7 +147,8 @@ struct CAAMEnvironmentContractLinuxTests { state: .recoveryRequired, targetProfile: "primary")) - let row = CAAMEnvironmentContract.rowState(configuration: configuration, snapshot: snapshot) + let row = CAAMEnvironmentContract.rowState( + configuration: configuration, snapshot: snapshot, now: snapshot.observedAt) #expect(row.availability == .recoveryRequired) #expect(row.canSwitch == false) diff --git a/TestsLinux/CAAMOperationReceiptLinuxTests.swift b/TestsLinux/CAAMOperationReceiptLinuxTests.swift new file mode 100644 index 0000000000..2956f8490c --- /dev/null +++ b/TestsLinux/CAAMOperationReceiptLinuxTests.swift @@ -0,0 +1,45 @@ +import Foundation +import Testing +@testable import CodexBarCore + +struct CAAMOperationReceiptLinuxTests { + @Test + func `receipt survives restart and binds complete configuration without raw results`() async throws { + let directory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) + defer { try? FileManager.default.removeItem(at: directory) } + let url = directory.appendingPathComponent("receipts.json") + let receipt = CAAMOperationReceipt( + configuration: CAAMControlFixtures.configuration, operationID: CAAMControlFixtures.operationID) + let store = CAAMFileReceiptStore(url: url) + try await store.save([receipt]) + let restarted = CAAMFileReceiptStore(url: url) + #expect(try await restarted.load() == [receipt]) + let attributes = try FileManager.default.attributesOfItem(atPath: url.path) + #expect((attributes[.posixPermissions] as? NSNumber)?.intValue == 0o600) + let object = try #require(JSONSerialization.jsonObject(with: Data(contentsOf: url)) as? [[String: Any]]) + #expect(Set(object[0].keys) == ["configuration", "operationID"]) + try await restarted.save([]) + #expect(try await store.load().isEmpty) + } + + @Test + func `oversized corrupt and duplicate receipt files fail closed`() async throws { + let directory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) + try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true) + defer { try? FileManager.default.removeItem(at: directory) } + let url = directory.appendingPathComponent("receipts.json") + let store = CAAMFileReceiptStore(url: url) + let receipt = CAAMOperationReceipt( + configuration: CAAMControlFixtures.configuration, operationID: CAAMControlFixtures.operationID) + let duplicate = try JSONEncoder().encode([receipt, receipt]) + for data in [Data(repeating: 32, count: 65537), Data("invalid fixture".utf8), duplicate] { + try data.write(to: url) + do { + _ = try await store.load() + Issue.record("Expected receipt storage rejection") + } catch { + #expect(!(error.localizedDescription.isEmpty)) + } + } + } +} diff --git a/TestsLinux/ObservabilityHubLinuxTests.swift b/TestsLinux/ObservabilityHubLinuxTests.swift new file mode 100644 index 0000000000..fbe75b6e6b --- /dev/null +++ b/TestsLinux/ObservabilityHubLinuxTests.swift @@ -0,0 +1,124 @@ +import Foundation +import Testing +@testable import CodexBarCore + +struct ObservabilityHubLinuxTests { + @Test + func `missing host telemetry remains unsupported without synthetic zeros`() { + let hub = ObservabilityHub.aggregate( + sessions: [CAAMControlFixtures.session()], providers: [], now: CAAMControlFixtures.now) + #expect(hub.services.count == 1) + #expect(hub.accounts.count == 2) + #expect(hub.providers.isEmpty) + #expect(hub.hosts.first?.evidence.availability == .unsupported) + #expect(hub.hosts.first?.evidence.observedAt == nil) + #expect(hub.hosts.first?.cpuPercent == nil) + #expect(hub.hosts.first?.memoryUsedBytes == nil) + #expect(hub.work.isEmpty) + } + + @Test + func `cost provenance currency window and metered component remain distinct`() throws { + let cost = Self.cost(provenance: .mixed) + let usage = UsageSnapshot( + primary: RateWindow(usedPercent: 25, windowMinutes: 300, resetsAt: nil, resetDescription: nil), + secondary: nil, updatedAt: CAAMControlFixtures.now) + let input = HubProviderInput(id: "fixture", label: "Fixture", source: "Fixture API", usage: usage, cost: cost) + let hub = ObservabilityHub.aggregate(sessions: [], providers: [input], now: CAAMControlFixtures.now) + let provider = try #require(hub.providers.first) + #expect(provider.primaryUsedPercent == 25) + #expect(provider.windowMinutes == 300) + #expect(provider.cost?.amount == 12) + #expect(provider.cost?.meteredAmount == 3) + #expect(provider.cost?.currency == "USD") + #expect(provider.cost?.historyDays == 30) + #expect(provider.cost?.provenance == .mixed) + #expect(provider.cost?.evidence.observedAt == CAAMControlFixtures.now) + } + + @Test + func `placeholder usage and invalid numeric costs remain unavailable`() { + let usage = UsageSnapshot( + primary: RateWindow( + usedPercent: 0, windowMinutes: 300, resetsAt: nil, resetDescription: nil, + isSyntheticPlaceholder: true), + secondary: nil, updatedAt: CAAMControlFixtures.now) + let cost = CostUsageTokenSnapshot( + sessionTokens: nil, sessionCostUSD: nil, last30DaysTokens: nil, + last30DaysCostUSD: .infinity, costProvenance: .unknown, daily: [], updatedAt: CAAMControlFixtures.now) + let input = HubProviderInput(id: "fixture", label: "Fixture", source: "Fixture", usage: usage, cost: cost) + let hub = ObservabilityHub.aggregate(sessions: [], providers: [input], now: CAAMControlFixtures.now) + #expect(hub.providers.first?.primaryUsedPercent == nil) + #expect(hub.providers.first?.evidence.availability == .partial) + #expect(hub.providers.first?.cost?.amount == nil) + } + + @Test + func `work records are lazy bounded historical and never identify live jobs`() { + let sessions = (0..<120).map { index in + CostUsageSessionBreakdown( + sessionID: "private-fixture-\(index)", lastActivity: CAAMControlFixtures.now, + inputTokens: nil, cachedInputTokens: nil, outputTokens: nil, + totalTokens: 10, requestCount: nil, costUSD: 0.1, modelBreakdowns: []) + } + let input = HubProviderInput( + id: "fixture", label: "Fixture", source: "Fixture local records", usage: nil, + cost: Self.cost(sessions: sessions)) + let overview = ObservabilityHub.aggregate(sessions: [], providers: [input], now: CAAMControlFixtures.now) + #expect(overview.work.isEmpty) + let work = ObservabilityHub.aggregate( + sessions: [], providers: [input], includeWork: true, now: CAAMControlFixtures.now) + #expect(work.work.count == 100) + #expect(work.workIsPartial) + #expect(!work.work.contains { $0.id.contains("private-fixture") }) + #expect(work.work.first?.evidence.availability == .partial) + let stale = ObservabilityHub.aggregate( + sessions: [], providers: [input], includeWork: true, + now: CAAMControlFixtures.now.addingTimeInterval(301)) + #expect(stale.work.first?.evidence.availability == .stale) + } + + @Test + func `account correlation requires matching provider and stable ID not profile name or email`() throws { + let first = CAAMControlFixtures.session() + let otherConfiguration = CAAMEnvironmentConfiguration(id: "other", label: "Other", connection: .ssh(destination: "fixture")) + var other = CAAMControlSession(configuration: otherConfiguration) + other.observe(CAAMControlFixtures.snapshot(provider: "other-provider"), now: CAAMControlFixtures.now) + let hub = ObservabilityHub.aggregate(sessions: [first, other], providers: [], now: CAAMControlFixtures.now) + let account = try #require(hub.accounts.first) + let otherAccount = try #require(hub.accounts.first { $0.environmentID == "other" }) + #expect(account.profile == otherAccount.profile) + #expect(account.displayLabel == otherAccount.displayLabel) + #expect(!account.isSameAccount(as: otherAccount)) + other.observe(CAAMControlFixtures.snapshot(), now: CAAMControlFixtures.now) + let matched = ObservabilityHub.aggregate(sessions: [other], providers: [], now: CAAMControlFixtures.now) + #expect(account.isSameAccount(as: try #require(matched.accounts.first))) + } + + @Test + func `Netdata navigation validates scheme credentials query and port without collection`() { + for value in [ + "https://metrics.example.invalid/dashboard", "http://127.0.0.1:19999", "http://localhost:19999", + ] { + #expect(NetdataReadOnlyFacade.dashboardURL(value) != nil) + } + for value in [ + "http://remote.example.invalid", "file:///tmp/fixture", "javascript:alert(1)", + "https://user:password@example.invalid", "https://example.invalid?token=fixture", + "https://example.invalid#fixture", "https://example.invalid:99999", " https://example.invalid", + "https://example.invalid\n", "https:///dashboard", String(repeating: "x", count: 2049), + ] { + #expect(NetdataReadOnlyFacade.dashboardURL(value) == nil) + } + } + + private static func cost( + provenance: CostProvenance = .listPriceEstimate, + sessions: [CostUsageSessionBreakdown] = []) -> CostUsageTokenSnapshot + { + CostUsageTokenSnapshot( + sessionTokens: nil, sessionCostUSD: nil, last30DaysTokens: 100, + last30DaysCostUSD: 12, meteredCostUSD: 3, costProvenance: provenance, + daily: [], sessions: sessions, updatedAt: CAAMControlFixtures.now) + } +} diff --git a/docs/work-notes/caam-environment-control.md b/docs/work-notes/caam-environment-control.md index 9af5b3c3ce..d0471bdab4 100644 --- a/docs/work-notes/caam-environment-control.md +++ b/docs/work-notes/caam-environment-control.md @@ -81,3 +81,213 @@ The coordinator is the sole Git owner of `/home/malcolmjones/Projects/CodexBar-w - The public `Jones-Systems/CodexBar` fork was created from upstream commit `392310c665485f8d57c93881e7416c5ebf69d8ef` and verified as a fork of `steipete/CodexBar`. PR #1 is the delivery vehicle inside the Jones Systems fork. Fork Actions are enabled for PR verification; the inherited scheduled upstream-monitor workflow remains disabled. - Merge of PR #1 completes the CodexBar foundation slice. Installation on the laptop, VPS, and Mac Mini; gateway provisioning; signing; release; automatic updates; and deployment remain outside this slice's authority and finish line. + +## Combined environment controls and observability hub — 2026-09-05 + +The owner authorizes one sequential implementation agent in `Jones-Systems/CodexBar`, +on `work/caam-codexbar-observability-hub`, through one draft PR against the fork's `main`. +No live account operation, installation, host-service change, upstream publication, or PR merge is authorized. +The historical foundation evidence above is not evidence for this new candidate. + +### Workspace synchronization and plan + +- Verified repository root, canonical `origin` (`github.com/Jones-Systems/CodexBar.git`), + task branch, process/worktree/Git ownership (same UID), clean status, and absence of merge, + rebase, cherry-pick, and revert state. Initial HEAD is `d6e929cd736eccae187cd41ddb5305a670afb2c8`. +- No uncommitted attributable work needed rescue. The existing foundation remains checkpointed + in that commit. Fetched only canonical `main`; it is the same commit and already reachable. + No merge, rebase, reset, clean, or force operation was needed. +- Read the synchronized root `AGENTS.md` (the only in-tree AGENTS file; `CLAUDE.md` resolves + to it), `VISION.md`, complete accepted control contract, engineering spec, and this Work Note. +- Sequential passes: reconcile implementation/contract and data sources; implement typed control + and read-only aggregation seams; integrate bounded responsive UI; add synthetic tests; run + affected checks; self-review correctness/security/maintainability/performance; publish a draft PR. +- Read-only dependency target: `Jones-Systems/CAAM`, + `work/caam-codexbar-gateway@8fdf44a1c2c3af0d36b934d79725608945790f32`. + Inaccessible external evidence will not block independent CodexBar work. + +## Connector and runtime incidents + +### C8 — Standard check entrypoint cannot launch Apple's catalog parser + +- Intended effect/action: run the required repository entrypoint `make check`. +- Native result: exit 2 (Make recipe error 1); Node reported `spawnSync plutil ENOENT`, errno -2, + no child PID/status/output. The bounded response was 328 tokens. +- Effect/postcondition: the script stopped at its first catalog check, before Swift lint-tool installation. + No tool was installed and no native format/lint result was obtained. +- Workaround: run the available portable feature-catalog checker, documentation-link checker, and index + checker directly. All exited 0; 111 feature keys in 23 catalogs, 192 documentation links, and the + existing `docs/llms.txt` index passed. `git diff --check` also exited 0. +- Blocked dependants: native catalog parsing and downstream standard check steps. Source review, + fixture authoring, portable validation, commits, and draft publication continue. +- Resolution: platform dependency remains absent; next diagnostic is existing CI against the candidate, + not tool installation on the bound host. + +### C9 — A later inspection process could no longer resolve ripgrep + +- Intended effect/action: inspect the staged core diff, list fixture tests, and find overlong Swift lines. +- Safe invocation: read-only `git diff --cached` followed by repository-scoped `rg` commands. +- Native result: staged diff returned normally; then Bash reported `rg: command not found`, exit 127. + The complete bounded response was 2931 tokens. Earlier `rg` invocations had succeeded; cause is unknown. +- Effect/postcondition: no repository mutation was requested by this invocation; already staged content + remains preserved. The later searches did not execute because the wrapper used `set -eu`. +- Workaround: use Git's built-in grep or standard grep for the remaining read-only inspection. + Native Swift checks were already unavailable independently; implementation and publication are unaffected. +- Resolution: fallback inspection pending confirmation. Next diagnostic: command availability in the same + bound workspace, without changing PATH, installing utilities, or inspecting credentials. + +### C7 — Inline locale maintenance could not obtain platform safety status + +- Intended effect: register missing feature strings as explicit English fallbacks without overwriting translations. +- Actions / safe shapes: connector `exec_command` with relative-path inline Python catalog edits; + then read-only Python and shell-loop key inspection. Three invocations returned the same + "couldn't determine the safety status" block, without a process exit, output, or session. +- Effect classification/postcondition: the intended edit was initially unconfirmed. A simple Git diff plus + English-catalog tail/count read exited 0 (88 output tokens), showed no catalog diff and the original + 1567-line file. Connector file read also confirmed the original modification time. No edit had occurred. +- Workaround: add the bounded, repository-local `Scripts/check_caam_observability_locales.py` through + `apply_patch`; invoke its explicit `--sync-locales` mode normally. Exit 0, 57 output tokens: + 73 new English keys across 23 catalogs; all 111 feature literal keys have matching format placeholders. +- Blocked dependants: catalog registration only. Source review and documentation continued; no private or + external data was requested. Resolution: resolved. The script's normal mode is read-only and its sync mode + reconciles each catalog independently after partial interruption. Next diagnostic: platform classification + of the original inline calls, not additional repository permissions. + +### C4 — Inspection glob did not match an existing source file + +- Intended effect/action: read-only search for account-scoped token-cache invalidation using `rg`. +- Safe shape: `rg Sources/CodexBar/UsageStore+Account* ...` inside the repository. +- Native evidence: `rg: ...UsageStore+Account*: No such file or directory (os error 2)`; + the multi-command wrapper exited 0, and the subcommand exit was not retained. +- Effect/postcondition: no mutation. The actual source was found under + `Sources/CodexBar/Providers/Codex/UsageStore+CodexAccountState.swift` and read successfully. +- Workaround/resolution: use the discovered exact path and `set -eu` for verification command groups. + No dependent implementation was blocked. Next diagnostic: preserve individual native status when grouping checks. +- Recurrence: a later read-only localization search included nonexistent + `Sources/CodexBarCore/Localization*`, returned exit 2 (86 tokens), while identifying the actual app + `Sources/CodexBar/Localization.swift`; that exact file was then read successfully. No mutation or blocked work. + +### C5 — Native test discovery cannot launch Swift + +- Intended effect/action: run the repository's isolated native test entrypoint, `make test`. +- Native result: connector command exit 2; Make reported test recipe error 1; Python raised + `FileNotFoundError: [Errno 2] No such file or directory: 'swift'`. +- Bounded evidence: test runner reported zero discovered selections, zero selected groups, and zero executed + groups. The combined inspection/test response was 3000 reported tokens; no full traceback is persisted here. +- Effect/postcondition: no native tests ran and no account operations were attempted. `swift` is absent from PATH + and the three checked conventional binary locations. No installation or credential inspection was attempted. +- Blocked dependants: local Swift compilation and native test results. Independent source review, fixtures, + portable checks, documentation, and publication continue. Existing Linux CI may provide native evidence; + existing macOS CI policy explicitly defers macOS tests on draft PRs. +- Resolution: toolchain limitation remains. Next diagnostic: native CI outcomes for the exact published HEAD. + +### C6 — Repository Git author identity is unset + +- Intended effect/action: verify commit attribution with `git var GIT_AUTHOR_IDENT`. +- Native result: exit 128, `Author identity unknown` / `fatal: empty ident name ... not allowed`. + The surrounding inspection response was 3461 reported tokens; host-derived fallback identity is omitted. +- Effect/postcondition: this read did not create a commit or modify Git configuration. +- Workaround: use command-scoped `-c user.name='ChatGPT' -c user.email='chatgpt-agent@example.invalid'` + for this agent's commits. Do not impersonate the owner or change global configuration. +- Blocked dependants: commit attribution only. Implementation and checks continued. Resolution will be + confirmed by the first successful attributed commit; next diagnostic is `git show` author/HEAD metadata. + +### C3 — Edit invocation had indeterminate platform safety status + +- Intended effect: refactor CAAM snapshot validation and freshness gating in one repository file. +- Action / safe invocation: connector `exec_command`, relative-path Python text edit of + `Sources/CodexBarCore/Providers/Codex/CAAM/CAAMEnvironmentValidation.swift`. +- Native result: "blocked by OpenAI because we couldn't determine the safety status of the request." + No process exit, output size, or session was returned. +- Effect classification: initially unconfirmed. Readback showed the original 350-line file with + its original modification time and unchanged decoder/row logic; the intended edit had not occurred. +- Workaround: after reconciliation, use the connector's repository `apply_patch` operation. + It exited 0 and explicitly reported the validation file and new typed validator as updated/added. +- Blocked dependants: only that edit. Model work was preserved; interface and test work continued. +- Resolution: resolved through an authorized patch, not a broader access path. Next diagnostic: + platform safety-classification details for the denied command; do not infer a repository permission failure. + +### C1 — Virtual path rejected before command launch + +- Intended effect: read-only identity, ownership, Git-state, and remote verification. +- Action / safe invocation: connector `exec_command`, bound workspace (handle omitted), + `workdir: /project`, command containing `stat /project` plus read-only Git queries. +- Native result: `INVALID_COMMAND_PATH`; tool `is_error: true`; explicitly reported + "No command was run." No process exit or command session was supplied; bounded error only. +- Effect classification / postcondition: known no effect; subsequent read confirmed exact initial + HEAD, clean status, and no Git operation in progress. +- Workaround: retain virtual `workdir`, use only relative shell paths (`stat .`). Retry exited 0 + with 233 reported output tokens and verified identity/ownership/state. +- Blocked dependants: initial verification only. Unaffected work continued immediately through + canonical fetch and guidance reads. +- Resolution: resolved. Next diagnostic on recurrence: distinguish virtual tool paths from native + shell paths; do not retry a mutation until its actual postcondition is reconciled. + +### C2 — Bundled read-only inspection rejected by platform safety + +- Intended effect: enumerate in-repository tooling/tests/guidance and read the exact external CAAM commit. +- Action / safe invocation: connector `exec_command` with relative repository searches, + `command -v`, and a conditional `gh api repos/Jones-Systems/CAAM/commits/` read. +- Native result: "This tool call was blocked by OpenAI's safety checks." No subprocess status, + exit code, output, or session was returned. The platform supplied no narrower cause. +- Effect classification: no repository mutation requested; command execution was not evidenced. + No external CAAM response was obtained. No bypass or equivalent external retry was attempted. +- Postcondition/workaround: connector file reads and a subsequent command restricted to CodexBar + inspection succeeded (exit 0, 332 output tokens). Status showed only this agent's Work Note edit. +- Blocked dependant: pinned CAAM implementation/conformance inspection. Independent contract, + interface, model, UI, fixture, cache, and test work continues from the accepted CodexBar contract. +- Resolution: repository inspection recovered; external dependency remains unverified. Next diagnostic: + owner-side platform rejection details, not broader credentials or recovery-bundle inspection. + +### Implementation reconciliation + +- The existing client executes only snapshots; mutation argv exists but typed plans/results and + execute-once coordination do not. Row capability flags ignore observation age, degraded reachability, + target eligibility, and the full recovery/status capability set. These are in-scope safety fixes. +- The shared subprocess result omits exit status. CAAM needs bounded error stdout on nonzero exit, + while retaining redacted diagnostics and rejecting success envelopes on failed commands. +- Existing `CostUsageTokenSnapshot` and `CostProvenance` are reusable read-only inputs. Local session + estimates, vendor-metered amounts, provider quota windows, and host-resource metrics must not be summed. +- No Netdata implementation, endpoint contract, fixture, or configured host was found in repository + source/docs/tests. Ship a typed unsupported facade and validated user-supplied dashboard deep links; + do not invent a Netdata API response or initiate network collection. +- `swift`, `swiftformat`, `swiftlint`, and `jq` are absent from PATH; `gh` and Python are available. + No toolchain installation is authorized. Native compile/lint results must come from existing CI or + remain explicitly unavailable; static checks do not substitute for them. + +### Implementation pass and early sequential self-review + +- Added typed plan/operation envelopes, semantic plan/result validation, native exit and UTF-8 status retention, + closed redacted error mapping, and a pure execute-once state machine. Unknown effects retain operation identity + through refresh, failed status lookup, and restart; fallback and interrupted-operation recovery are separate. +- Added source-age mutation gates (60 seconds, 5-second future-skew allowance) and a 15-minute bounded + stale-while-revalidate display cache. Refresh coalesces, publishes completed environments incrementally, + and suppresses results for changed configuration generations. +- Added an actor-isolated receipt store containing only validated configuration selectors and operation UUIDs. + Receipt persistence precedes launch; storage failure blocks launch/qualification. A single application + coordinator serializes operations and receipt writes; independently constructed test coordinators use fixtures. +- Self-review found potential cross-environment receipt-write races, ID-only qualification reuse after an endpoint + edit, stale configuration publication, and loss of an unresolved operation after a snapshot with an empty journal. + Remediated with global operation serialization, full-configuration qualification/binding, generation checks, + and retained-operation lookup that never treats snapshot absence as terminal evidence. +- Self-review also found display helpers that can read credential-adjacent disk caches. The hub deliberately avoids + those helpers. It reads existing in-memory provider publications, labels cost as historical/unattributed to CAAM + accounts, and preserves currency, window, estimate/metered provenance, source timestamps, and confidence. +- Overview, Accounts, Systems, and Work are compact views on distinct typed ledgers. Work materialization is lazy + and capped at 100 records. No combined utilization/cost total, live-job inference, host metric, or Netdata API is invented. +- Existing test-process detection disables implicit CAAM transports and real receipt-file access. New tests use + synthetic runners, fixed clocks, memory receipts or temporary fixture-only directories, and no live credentials. + +### Core checkpoint review and check outcomes + +- Sequential correctness self-review: checked command/response bindings, single-use confirmation, fresh terminal + postconditions, target matching, no-effect versus unknown-effect distinction, and recovery/fallback separation. + Existing fixed-date projection tests now inject their observation date rather than depending on wall-clock age. +- Sequential security self-review: checked exact closed argv, rejected relative PATH entries, credential-field + rejection, error-text redaction, bounded decoding/receipt IO, same-operation lookup, and safe dashboard URLs. +- Sequential maintainability/performance self-review: transport, pure state, persistence, aggregation, and UI are + separate seams; all arrays/caches/details have explicit bounds; collectors do not read credentials or initiate + new host/provider scans. No independent review or measured performance result is claimed. +- Added fixture test cases remain unexecuted because Swift is absent. `make test` and `make check` are attempted + but blocked as C5/C8 describe. Portable catalog coverage, documentation links/index, and whitespace checks pass. +- No live CAAM, SSH account operation, Netdata collection, credential inspection, installation, or deployment ran. From db5e39a36be393724d6efbd85addc5085054a098 Mon Sep 17 00:00:00 2001 From: ChatGPT Date: Sun, 6 Sep 2026 02:49:09 +0200 Subject: [PATCH 2/9] Connect safe environment workflows to the observability hub --- Scripts/check_caam_observability_locales.py | 87 ++++++ .../test_check_caam_observability_locales.py | 67 ++++ .../CAAMEnvironmentControlsView.swift | 144 +++++++++ .../CodexBar/CAAMEnvironmentCoordinator.swift | 287 +++++++++++++++--- .../CodexBar/CodexObservabilityHubView.swift | 269 ++++++++++++++++ .../PreferencesCodexEnvironmentsSection.swift | 35 ++- ...eferencesProvidersPane+Observability.swift | 29 ++ .../CodexBar/PreferencesProvidersPane.swift | 42 ++- .../Resources/ar.lproj/Localizable.strings | 75 +++++ .../Resources/ca.lproj/Localizable.strings | 75 +++++ .../Resources/de.lproj/Localizable.strings | 75 +++++ .../Resources/en.lproj/Localizable.strings | 75 +++++ .../Resources/es.lproj/Localizable.strings | 75 +++++ .../Resources/fa.lproj/Localizable.strings | 75 +++++ .../Resources/fr.lproj/Localizable.strings | 75 +++++ .../Resources/gl.lproj/Localizable.strings | 75 +++++ .../Resources/id.lproj/Localizable.strings | 75 +++++ .../Resources/it.lproj/Localizable.strings | 75 +++++ .../Resources/ja.lproj/Localizable.strings | 75 +++++ .../Resources/ko.lproj/Localizable.strings | 75 +++++ .../Resources/nl.lproj/Localizable.strings | 75 +++++ .../Resources/pl.lproj/Localizable.strings | 75 +++++ .../Resources/pt-BR.lproj/Localizable.strings | 75 +++++ .../Resources/ru.lproj/Localizable.strings | 75 +++++ .../Resources/sv.lproj/Localizable.strings | 75 +++++ .../Resources/th.lproj/Localizable.strings | 75 +++++ .../Resources/tr.lproj/Localizable.strings | 75 +++++ .../Resources/uk.lproj/Localizable.strings | 75 +++++ .../Resources/vi.lproj/Localizable.strings | 75 +++++ .../zh-Hans.lproj/Localizable.strings | 75 +++++ .../zh-Hant.lproj/Localizable.strings | 75 +++++ .../Observability/NetdataReadOnlyFacade.swift | 3 +- .../Observability/ObservabilityHub.swift | 3 +- .../CAAMEnvironmentCoordinatorTests.swift | 200 ++++++++++++ .../CAAMEnvironmentSettingsTests.swift | 12 +- TestsLinux/CAAMControlFixtures.swift | 3 +- TestsLinux/ObservabilityHubLinuxTests.swift | 3 +- .../caam-environment-control.md | 103 ++++++- docs/work-notes/caam-environment-control.md | 48 +++ 39 files changed, 2996 insertions(+), 64 deletions(-) create mode 100644 Scripts/check_caam_observability_locales.py create mode 100644 Scripts/test_check_caam_observability_locales.py create mode 100644 Sources/CodexBar/CAAMEnvironmentControlsView.swift create mode 100644 Sources/CodexBar/CodexObservabilityHubView.swift create mode 100644 Sources/CodexBar/PreferencesProvidersPane+Observability.swift create mode 100644 Tests/CodexBarTests/CAAMEnvironmentCoordinatorTests.swift diff --git a/Scripts/check_caam_observability_locales.py b/Scripts/check_caam_observability_locales.py new file mode 100644 index 0000000000..471470e549 --- /dev/null +++ b/Scripts/check_caam_observability_locales.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python3 +"""Check feature literal-key coverage without requiring Apple's plutil. + +This bounded source/catalog check does not replace native strings parsing or Swift tests. +The explicit --sync-locales maintenance mode adds only missing keys, with labeled English +fallbacks; it never claims to translate text or changes existing translations. +""" + +from __future__ import annotations + +import argparse +from pathlib import Path +import re + + +ROOT = Path(__file__).resolve().parents[1] +RESOURCES = ROOT / "Sources/CodexBar/Resources" +SOURCE_NAMES = ( + "CAAMEnvironmentControlsView.swift", + "CAAMEnvironmentCoordinator.swift", + "CodexObservabilityHubView.swift", + "PreferencesCodexEnvironmentsSection.swift", + "PreferencesProvidersPane.swift", +) +LOCALES = ( + "en", "ar", "ca", "de", "es", "fa", "fr", "gl", "id", "it", "ja", "ko", "nl", + "pl", "pt-BR", "ru", "sv", "th", "tr", "uk", "vi", "zh-Hans", "zh-Hant", +) +LITERAL = re.compile(r'\bL\(\s*"((?:[^"\\]|\\.)*)"\s*\)') +ENTRY = re.compile(r'^"((?:[^"\\]|\\.)*)"\s*=\s*"((?:[^"\\]|\\.)*)";', re.MULTILINE) +PLACEHOLDER = re.compile(r'%(?:(\d+)\$)?[-+ #0]*\d*(?:\.\d+)?(?:hh|ll|[hlLzjt])?[@diuoxXfFeEgGaAcsp]') + + +def bounded_read(path: Path) -> str: + if not path.resolve().is_relative_to(ROOT) or path.stat().st_size > 1024 * 1024: + raise ValueError("Source or catalog is outside the repository or exceeds the size limit") + return path.read_text(encoding="utf-8") + + +def signature(value: str) -> list[str]: + value = value.replace("%%", "") + return sorted(re.sub(r'^%\d+\$', "%", match.group(0)) for match in PLACEHOLDER.finditer(value)) + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--sync-locales", action="store_true", help="append explicitly labeled English fallback keys") + options = parser.parse_args() + keys: set[str] = set() + for name in SOURCE_NAMES: + keys.update(LITERAL.findall(bounded_read(ROOT / "Sources/CodexBar" / name))) + english_path = RESOURCES / "en.lproj/Localizable.strings" + english = dict(ENTRY.findall(bounded_read(english_path))) + new_keys = sorted(keys - english.keys()) + if options.sync_locales: + for locale in LOCALES: + path = RESOURCES / f"{locale}.lproj/Localizable.strings" + text = bounded_read(path) + existing = dict(ENTRY.findall(text)) + # Reconcile every feature key so interruption after the English write is restart-safe. + missing = sorted(keys - existing.keys()) + if missing: + addition = "\n/* CAAM observability: English fallback pending translation review. */\n" + addition += "".join(f'"{key}" = "{english.get(key, key)}";\n' for key in missing) + path.write_text(text.rstrip() + "\n" + addition, encoding="utf-8") + print(f"Registered {len(new_keys)} new English keys across {len(LOCALES)} catalogs (not translations).") + english = dict(ENTRY.findall(bounded_read(english_path))) + failures: list[str] = [] + for locale in LOCALES: + entries = dict(ENTRY.findall(bounded_read(RESOURCES / f"{locale}.lproj/Localizable.strings"))) + for key in sorted(keys): + value = entries.get(key) + if value is None: + failures.append(f"{locale}: missing {key}") + elif signature(value) != signature(english.get(key, key)): + failures.append(f"{locale}: placeholder mismatch {key}") + if failures: + print("\n".join(failures[:30])) + print(f"FAILED: {len(failures)} coverage/placeholder issues.") + return 1 + print(f"PASS: {len(keys)} feature literal keys covered in {len(LOCALES)} catalogs; placeholders match.") + print("Native plutil parsing, linguistic review, and Swift execution are separate checks.") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/Scripts/test_check_caam_observability_locales.py b/Scripts/test_check_caam_observability_locales.py new file mode 100644 index 0000000000..706ccb2872 --- /dev/null +++ b/Scripts/test_check_caam_observability_locales.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python3 +"""Portable fixture tests for the feature's catalog maintenance helper, not Swift behavior.""" + +import contextlib +import io +from pathlib import Path +import tempfile +import unittest +from unittest.mock import patch + +import check_caam_observability_locales as checker + + +class CatalogHelperTests(unittest.TestCase): + def test_literal_keys_include_multiline_calls_and_escaped_quotes(self): + text = 'L("One")\nL(\n "Two"\n)\nL("A \\"quote\\"")\nL(variable)' + self.assertEqual(checker.LITERAL.findall(text), ["One", "Two", 'A \\"quote\\"']) + + def test_placeholder_positions_normalize_without_counting_literal_percent(self): + self.assertEqual(checker.signature("%2$d · %1$@ · 100%%"), checker.signature("%@ · %d")) + self.assertNotEqual(checker.signature("%.1f"), checker.signature("%d")) + + def test_bounded_read_rejects_outside_root(self): + with tempfile.TemporaryDirectory() as directory: + outside = Path(directory) / "outside" + outside.write_text("fixture", encoding="utf-8") + with self.assertRaises(ValueError): + checker.bounded_read(outside) + + def test_bounded_read_rejects_oversized_file(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + fixture = root / "large" + fixture.write_bytes(b"x" * (1024 * 1024 + 1)) + with patch.object(checker, "ROOT", root), self.assertRaises(ValueError): + checker.bounded_read(fixture) + + def test_sync_recovers_after_english_written_and_preserves_translation(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + resources = root / "Sources/CodexBar/Resources" + for locale in ("en", "de"): + (resources / f"{locale}.lproj").mkdir(parents=True) + source = root / "Sources/CodexBar/Fixture.swift" + source.write_text('L("Existing")\nL("Missing %@")', encoding="utf-8") + english = resources / "en.lproj/Localizable.strings" + german = resources / "de.lproj/Localizable.strings" + english.write_text('"Existing" = "Existing";\n"Missing %@" = "Missing %@";\n', encoding="utf-8") + german.write_text('"Existing" = "Vorhanden";\n', encoding="utf-8") + with ( + patch.object(checker, "ROOT", root), + patch.object(checker, "RESOURCES", resources), + patch.object(checker, "SOURCE_NAMES", ("Fixture.swift",)), + patch.object(checker, "LOCALES", ("en", "de")), + patch("sys.argv", ["check", "--sync-locales"]), + contextlib.redirect_stdout(io.StringIO()), + ): + self.assertEqual(checker.main(), 0) + first = german.read_bytes() + self.assertEqual(checker.main(), 0) + self.assertEqual(german.read_bytes(), first) + self.assertIn('"Existing" = "Vorhanden";', first.decode("utf-8")) + self.assertIn('"Missing %@" = "Missing %@";', first.decode("utf-8")) + + +if __name__ == "__main__": + unittest.main() diff --git a/Sources/CodexBar/CAAMEnvironmentControlsView.swift b/Sources/CodexBar/CAAMEnvironmentControlsView.swift new file mode 100644 index 0000000000..b20e1817db --- /dev/null +++ b/Sources/CodexBar/CAAMEnvironmentControlsView.swift @@ -0,0 +1,144 @@ +import CodexBarCore +import SwiftUI + +@MainActor +struct CAAMEnvironmentControlsView: View { + let configuration: CAAMEnvironmentConfiguration + @Bindable var coordinator: CAAMEnvironmentCoordinator + + private var session: CAAMControlSession { self.coordinator.session(for: self.configuration) } + + var body: some View { + let session = self.session + let row = session.row(now: Date()) + VStack(alignment: .leading, spacing: 6) { + HStack { + Menu(L("Plan switch")) { + ForEach(session.snapshot?.profiles.filter { $0.eligible && !$0.active } ?? []) { profile in + Button(profile.name) { + Task { + await self.coordinator.plan(configuration: self.configuration, target: profile.name) + } + } + } + } + .disabled(!row.canSwitch || self.coordinator.isRefreshing || session.phase.isBusy) + Button(L("Plan fallback")) { + guard let target = session.snapshot?.fallbackProfile else { return } + Task { + await self.coordinator.plan(configuration: self.configuration, target: target, fallback: true) + } + } + .disabled(!row.canRestoreFallback || self.coordinator.isRefreshing || session.phase.isBusy) + if session.pendingOperationID != nil { + Button(L("Look up operation")) { + Task { await self.coordinator.lookup(configuration: self.configuration) } + } + .disabled(session.phase.isBusy || self.coordinator.isRefreshing) + Button(L("Review recovery")) { + self.coordinator.prepareRecovery(configuration: self.configuration) + } + .disabled(!row.canRecover || session.phase.isBusy || self.coordinator.isRefreshing) + } + } + .buttonStyle(.bordered) + .controlSize(.small) + .disabled(self.coordinator.isOperating) + + if session.phase.isBusy { + HStack { + ProgressView().controlSize(.small) + Text(L("Waiting for CAAM; the app remains available.")) + } + .font(.caption) + } + if let operationID = session.pendingOperationID { + Text(String(format: L("Unresolved operation: %@"), operationID.uuidString.lowercased())) + .font(.caption2.monospaced()) + .textSelection(.enabled) + } + if session.phase == .unknown { + Text(L("Operation outcome is unresolved. Look up the existing operation before another change.")) + .font(.caption).foregroundStyle(.orange) + } + if session.phase == .manualRequired { + Text(L("CAAM requires manual intervention on this environment. Automatic recovery is disabled.")) + .font(.caption).foregroundStyle(.orange) + } + if let failure = session.failure { + Text(failure.localizedDescription).font(.caption).foregroundStyle(.orange) + if let code = failure.code { + Text(String(format: L("Gateway code: %@"), code)).font(.caption2.monospaced()) + } + } + if let result = session.lastResult { + Text(String(format: L("Last operation: %@ · %@"), result.state.rawValue, result.effect.rawValue)) + .font(.caption) + } + if let confirmation = session.confirmation { + self.confirmationView(confirmation) + } else if !self.coordinator.mutationsQualified(for: self.configuration) { + Text(L("Read-only until CAAM conformance is independently qualified.")) + .font(.caption) + .foregroundStyle(.secondary) + } + } + } + + private func confirmationView(_ confirmation: CAAMControlConfirmation) -> some View { + GroupBox { + VStack(alignment: .leading, spacing: 6) { + Text(self.confirmationTitle(confirmation.kind)).font(.caption.weight(.semibold)) + Text(String(format: L("Expected revision: %@"), confirmation.revision)).font(.caption.monospaced()) + if let plan = confirmation.plan { + Text(String( + format: L("Change host default: %@ → %@"), + plan.currentProfile ?? "—", + plan.targetProfile)) + Text(String(format: L("Plan digest: %@"), plan.planDigest)).font(.caption2.monospaced()) + Text(plan.expiresAt, style: .time).font(.caption) + DisclosureGroup(L("Affected profiles")) { + ScrollView { + Text(plan.affectedProfiles.joined(separator: ", ")) + .font(.caption).frame(maxWidth: .infinity, alignment: .leading) + } + .frame(maxHeight: 100) + } + if plan.reloadRequired { + Text(L("A running Codex process may need to reload.")).font(.caption) + } + } else { + Text(L("CAAM may complete or roll back its pending transaction. This does not restore fallback.")) + .font(.caption) + } + HStack { + Button(L("Confirm once")) { + Task { + await self.coordinator.confirm( + configuration: self.configuration, + confirmationID: confirmation.id) + } + } + .disabled(!self.coordinator.mutationsQualified(for: self.configuration) || + self.coordinator.isOperating || self.coordinator.isRefreshing) + Button(L("Cancel")) { self.coordinator.cancelConfirmation(configuration: self.configuration) } + } + .buttonStyle(.bordered) + .controlSize(.small) + if !self.coordinator.mutationsQualified(for: self.configuration) { + Text(L("Read-only until CAAM conformance is independently qualified.")) + .font(.caption).foregroundStyle(.secondary) + } + } + .frame(maxWidth: .infinity, alignment: .leading) + } + } + + private func confirmationTitle(_ kind: CAAMControlConfirmation.Kind) -> String { + switch kind { + case .switchProfile: L("Review switch plan") + case .restoreFallback: L("Review fallback restoration") + case .recoverSwitch: L("Review interrupted-operation recovery") + } + } +} diff --git a/Sources/CodexBar/CAAMEnvironmentCoordinator.swift b/Sources/CodexBar/CAAMEnvironmentCoordinator.swift index 14fd62b164..2204197d4d 100644 --- a/Sources/CodexBar/CAAMEnvironmentCoordinator.swift +++ b/Sources/CodexBar/CAAMEnvironmentCoordinator.swift @@ -5,76 +5,289 @@ import Observation @MainActor @Observable final class CAAMEnvironmentCoordinator { + private static var applicationInstance: CAAMEnvironmentCoordinator? @ObservationIgnored private let client: CAAMEnvironmentClient + @ObservationIgnored private let receiptStore: any CAAMOperationReceiptStoring + @ObservationIgnored private let now: @Sendable () -> Date + @ObservationIgnored private let qualifiedConfigurations: [CAAMEnvironmentConfiguration] @ObservationIgnored private var configurationGeneration = 0 - @ObservationIgnored private var observedConfigurationsByID: [String: CAAMEnvironmentConfiguration] = [:] - private(set) var rowsByID: [String: CAAMEnvironmentRowState] = [:] + @ObservationIgnored private var receipts: [String: CAAMOperationReceipt] = [:] + @ObservationIgnored private var receiptsLoaded = false + private(set) var sessionsByID: [String: CAAMControlSession] = [:] private(set) var isRefreshing = false + private(set) var isOperating = false private(set) var notice: String? + private(set) var receiptStorageAvailable = true - init(client: CAAMEnvironmentClient = CAAMEnvironmentClient()) { + init( + client: CAAMEnvironmentClient = CAAMEnvironmentClient(), + receiptStore: any CAAMOperationReceiptStoring = CAAMMemoryReceiptStore(), + qualifiedConfigurations: [CAAMEnvironmentConfiguration] = [], + now: @escaping @Sendable () -> Date = { Date() }) + { self.client = client + self.receiptStore = receiptStore + self.qualifiedConfigurations = qualifiedConfigurations + self.now = now } - func state(configurations: [CAAMEnvironmentConfiguration]) -> CodexEnvironmentsSectionState { + static func applicationCoordinator() -> CAAMEnvironmentCoordinator { + if SettingsStore.isRunningTests { + return CAAMEnvironmentCoordinator(client: CAAMEnvironmentClient( + runner: CAAMTestProcessNoLaunchRunner(), environment: [:], resolveLocalGateway: { _ in nil })) + } + if let instance = self.applicationInstance { return instance } + let directory = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first + ?? FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent("Library/Application Support") + let url = directory.appendingPathComponent("CodexBar/caam-operation-receipts.json") + // No environment is qualified merely because it advertises capability strings. + let instance = CAAMEnvironmentCoordinator(receiptStore: CAAMFileReceiptStore(url: url)) + self.applicationInstance = instance + return instance + } + + func session(for configuration: CAAMEnvironmentConfiguration) -> CAAMControlSession { + let receipt = self.receipts[configuration.id] + if let session = self.sessionsByID[configuration.id], session.configuration == configuration { + if let receipt, receipt.configuration == configuration, session.pendingOperationID == nil { + var restored = CAAMControlSession(configuration: configuration, pendingOperationID: receipt.operationID) + if let snapshot = session.snapshot, let receivedAt = session.receivedAt { + restored.observe(snapshot, now: receivedAt) + } + return restored + } + return session + } + return CAAMControlSession( + configuration: configuration, + pendingOperationID: receipt?.configuration == configuration ? receipt?.operationID : nil) + } + + func state(configurations: [CAAMEnvironmentConfiguration], at date: Date? = nil) -> CodexEnvironmentsSectionState { let displayOrder = configurations.filter { $0.connection.kind == .local } + configurations.filter { $0.connection.kind == .ssh } - let rows = displayOrder.map { configuration in - let observedRow = self.observedConfigurationsByID[configuration.id] == configuration - ? self.rowsByID[configuration.id] - : nil - return observedRow ?? CAAMEnvironmentContract.unavailableRow( - configuration: configuration, - message: L("Not checked yet.")) - } return CodexEnvironmentsSectionState( configurations: configurations, - rows: rows, + rows: displayOrder.map { self.session(for: $0).row(now: date ?? self.now()) }, isRefreshing: self.isRefreshing, notice: self.notice) } + func canChangeConfigurations( + from old: [CAAMEnvironmentConfiguration], + to new: [CAAMEnvironmentConfiguration]) -> Bool + { + guard !self.isRefreshing, !self.isOperating else { return false } + return old.allSatisfy { configuration in + if new.contains(configuration) { return true } + return self.receiptsLoaded && self.receiptStorageAvailable && self.session(for: configuration).canForget && + self.receipts[configuration.id] == nil + } + } + + func retainedConfigurations( + outside configurations: [CAAMEnvironmentConfiguration]) -> [CAAMEnvironmentConfiguration] + { + self.receipts.values.map(\.configuration) + .filter { !configurations.contains($0) } + .sorted { $0.id < $1.id } + } + func configurationsDidChange(_ configurations: [CAAMEnvironmentConfiguration]) { self.configurationGeneration &+= 1 - let configurationsByID = Dictionary(uniqueKeysWithValues: configurations.map { ($0.id, $0) }) - self.rowsByID = self.rowsByID.filter { id, _ in - self.observedConfigurationsByID[id] == configurationsByID[id] - } - self.observedConfigurationsByID = self.observedConfigurationsByID.filter { id, configuration in - configurationsByID[id] == configuration + self.sessionsByID = self.sessionsByID.filter { _, session in + configurations.contains(session.configuration) || !session.canForget } self.notice = nil } - func refresh(configurations: [CAAMEnvironmentConfiguration]) async { - guard !self.isRefreshing else { return } - do { - _ = try CAAMEnvironmentContract.validateConfigurations(configurations) - } catch { - self.notice = error.localizedDescription + func mutationsQualified(for configuration: CAAMEnvironmentConfiguration) -> Bool { + self.qualifiedConfigurations.contains(configuration) && self.receiptStorageAvailable && self.receiptsLoaded && + (self.receipts[configuration.id] == nil || self.receipts[configuration.id]?.configuration == configuration) + } + + func refresh(configurations: [CAAMEnvironmentConfiguration], force: Bool = true) async { + guard !self.isRefreshing, !self.isOperating else { return } + guard (try? CAAMEnvironmentContract.validateConfigurations(configurations)) != nil else { + self.notice = L("The CAAM configuration is invalid.") return } - self.isRefreshing = true self.notice = nil defer { self.isRefreshing = false } let generation = self.configurationGeneration - - var refreshed: [String: CAAMEnvironmentRowState] = [:] + await self.loadReceipts() + self.sessionsByID = self.sessionsByID.filter { _, session in + configurations.contains(session.configuration) || !session.canForget + } for configuration in configurations { + guard generation == self.configurationGeneration, !Task.isCancelled else { return } + guard self.sessionsByID[configuration.id] != nil || + self.sessionsByID.count < CAAMEnvironmentContract.maximumEnvironmentCount + else { + self.notice = L("Resolve retained operations before adding more environment observations.") + continue + } + var session = self.session(for: configuration) + if !force, session.row(now: self.now()).availability == .ready { continue } do { let snapshot = try await self.client.fetchSnapshot(for: configuration) - refreshed[configuration.id] = CAAMEnvironmentContract.rowState( - configuration: configuration, - snapshot: snapshot) + session.observe(snapshot, now: self.now()) } catch { - refreshed[configuration.id] = CAAMEnvironmentContract.unavailableRow( - configuration: configuration, - message: error.localizedDescription) + session.failRefresh(CAAMEnvironmentClient.failure(for: error, possibleEffect: .knownNoEffect)) + } + guard generation == self.configurationGeneration else { return } + // Publish each completed environment without waiting for slower hosts; old rows remain visible meanwhile. + self.sessionsByID[configuration.id] = session + if let operationID = session.pendingOperationID, self.receipts[configuration.id] == nil { + self.receipts[configuration.id] = CAAMOperationReceipt( + configuration: configuration, operationID: operationID) + await self.saveReceipts() + } + } + if !self.receiptStorageAvailable, self.receiptsLoaded { await self.saveReceipts() } + if self.receipts.values.contains(where: { !configurations.contains($0.configuration) }) { + self.notice = L( + "An unresolved operation belongs to a changed environment. Restore its original configuration.") + } + } + + func plan(configuration: CAAMEnvironmentConfiguration, target: String, fallback: Bool = false) async { + guard self.canStart(configuration) else { return } + self.isOperating = true + defer { self.isOperating = false } + let generation = self.configurationGeneration + var session = self.session(for: configuration) + do { + let revision = try session.beginPlan(target: target, fallback: fallback, now: self.now()) + self.sessionsByID[configuration.id] = session + let plan = try await self.client.fetchPlan(for: configuration, profile: target, expectedRevision: revision) + guard generation == self.configurationGeneration else { + throw CAAMControlFailure(reason: .stale) } + try session.acceptPlan(plan, now: self.now()) + } catch { + session.failPlan((error as? CAAMControlFailure) ?? + CAAMEnvironmentClient.failure(for: error, possibleEffect: .knownNoEffect)) } - guard generation == self.configurationGeneration else { return } - self.rowsByID = refreshed - self.observedConfigurationsByID = Dictionary(uniqueKeysWithValues: configurations.map { ($0.id, $0) }) + self.sessionsByID[configuration.id] = session + } + + func prepareRecovery(configuration: CAAMEnvironmentConfiguration) { + guard self.canStart(configuration) else { return } + var session = self.session(for: configuration) + do { + try session.prepareRecovery(now: self.now()) + } catch { + self.notice = CAAMControlFailure(reason: .stale).localizedDescription + } + self.sessionsByID[configuration.id] = session + } + + func cancelConfirmation(configuration: CAAMEnvironmentConfiguration) { + self.sessionsByID[configuration.id]?.cancelConfirmation() + } + + func confirm(configuration: CAAMEnvironmentConfiguration, confirmationID: UUID) async { + guard self.canStart(configuration) else { return } + self.isOperating = true + defer { self.isOperating = false } + var session = self.session(for: configuration) + let operation: CAAMGatewayOperation + do { + operation = try session.confirm( + id: confirmationID, mutationsQualified: self.mutationsQualified(for: configuration), now: self.now()) + } catch { + self.notice = (error as? CAAMControlFailure)?.localizedDescription + return + } + self.sessionsByID[configuration.id] = session + guard let operationID = operation.operationID else { return } + self.receipts[configuration.id] = CAAMOperationReceipt(configuration: configuration, operationID: operationID) + // Persist operation identity before any possible gateway launch, including after app restart. + await self.saveReceipts() + guard self.receiptStorageAvailable else { + session.failOperation(CAAMControlFailure(reason: .unavailable), operation: operation) + if session.pendingOperationID == nil { self.receipts.removeValue(forKey: configuration.id) } + // No gateway was launched. A later refresh retries the desired receipt image before qualification + // can reopen, removing any intent that an atomic write may have installed before reporting failure. + self.sessionsByID[configuration.id] = session + return + } + await self.run(operation, configuration: configuration) + } + + func lookup(configuration: CAAMEnvironmentConfiguration) async { + guard self.canStart(configuration) else { return } + self.isOperating = true + defer { self.isOperating = false } + var session = self.session(for: configuration) + do { + let operation = try session.beginLookup() + self.sessionsByID[configuration.id] = session + await self.run(operation, configuration: configuration) + } catch { + self.notice = CAAMControlFailure(reason: .busy).localizedDescription + } + } + + private func canStart(_ configuration: CAAMEnvironmentConfiguration) -> Bool { + guard !self.isRefreshing, !self.isOperating, !self.session(for: configuration).phase.isBusy, + self.receipts[configuration.id] == nil || self.receipts[configuration.id]?.configuration == configuration + else { + self.notice = CAAMControlFailure(reason: .busy).localizedDescription + return false + } + return true + } + + private func run(_ operation: CAAMGatewayOperation, configuration: CAAMEnvironmentConfiguration) async { + guard var session = self.sessionsByID[configuration.id] else { return } + do { + let result = try await self.client.perform(operation, for: configuration) + try session.acceptResult(result, now: self.now()) + } catch { + let failure = (error as? CAAMControlFailure) ?? + CAAMControlFailure(reason: .invalidResponse, effect: .unknown) + session.failOperation(failure, operation: operation) + } + self.sessionsByID[configuration.id] = session + if session.pendingOperationID == nil { + self.receipts.removeValue(forKey: configuration.id) + await self.saveReceipts() + } + } + + private func loadReceipts() async { + guard !self.receiptsLoaded else { return } + do { + let receipts = try await self.receiptStore.load() + _ = try CAAMEnvironmentContract.validateConfigurations(receipts.map(\.configuration)) + self.receipts = Dictionary(uniqueKeysWithValues: receipts.map { ($0.configuration.id, $0) }) + self.receiptsLoaded = true + self.receiptStorageAvailable = true + } catch { + self.receiptStorageAvailable = false + self.notice = L("CAAM operation tracking is unavailable. Mutations remain disabled.") + } + } + + private func saveReceipts() async { + guard self.receiptsLoaded else { return } + do { + try await self.receiptStore.save(self.receipts.values.sorted { $0.configuration.id < $1.configuration.id }) + self.receiptStorageAvailable = true + } catch { + self.receiptStorageAvailable = false + self.notice = L("CAAM operation tracking is unavailable. Mutations remain disabled.") + } + } +} + +/// Existing test-process detection must suppress all implicit transport and real receipt-store access. +/// Tests that exercise transport coordination inject their own synthetic client and receipt store instead. +private struct CAAMTestProcessNoLaunchRunner: CAAMEnvironmentCommandRunning { + func run(command _: CAAMEnvironmentCommand, environment _: [String: String]) async throws -> SubprocessResult { + throw SubprocessRunnerError.binaryNotFound("CAAM disabled in test process") } } diff --git a/Sources/CodexBar/CodexObservabilityHubView.swift b/Sources/CodexBar/CodexObservabilityHubView.swift new file mode 100644 index 0000000000..c21ad7b058 --- /dev/null +++ b/Sources/CodexBar/CodexObservabilityHubView.swift @@ -0,0 +1,269 @@ +import CodexBarCore +import Foundation +import SwiftUI + +private enum ObservabilityHubTab: String, CaseIterable, Identifiable { + case overview + case accounts + case systems + case work + + var id: String { self.rawValue } + + var title: String { + switch self { + case .overview: L("Overview") + case .accounts: L("Accounts") + case .systems: L("Systems") + case .work: L("Work") + } + } +} + +@MainActor +struct CodexObservabilityHubView: View { + @Bindable var coordinator: CAAMEnvironmentCoordinator + let configurations: [CAAMEnvironmentConfiguration] + let providerInputs: () -> [HubProviderInput] + @State private var tab: ObservabilityHubTab = .overview + + var body: some View { + Section { + Picker(L("Observability view"), selection: self.$tab) { + ForEach(ObservabilityHubTab.allCases) { tab in + Text(tab.title).tag(tab) + } + } + .pickerStyle(.segmented) + TimelineView(.periodic(from: .now, by: 15)) { context in + let snapshot = ObservabilityHub.aggregate( + sessions: self.configurations.map { self.coordinator.session(for: $0) }, + providers: self.providerInputs(), + includeWork: self.tab == .work, + now: context.date) + ScrollView { + LazyVStack(alignment: .leading, spacing: 12) { + switch self.tab { + case .overview: self.overview(snapshot) + case .accounts: self.accounts(snapshot) + case .systems: self.systems(snapshot) + case .work: self.work(snapshot) + } + } + .frame(maxWidth: .infinity, alignment: .leading) + } + .frame(minHeight: 160, maxHeight: 380) + } + } header: { + Text(L("Observability hub")) + } footer: { + SettingsSectionFooter( + L("Local service state, provider usage and cost, and host resources are separate ledgers.")) + } + } + + private func overview(_ snapshot: ObservabilityHubSnapshot) -> some View { + VStack(alignment: .leading, spacing: 10) { + Text(L("Service and local state")).font(.subheadline.weight(.semibold)) + if snapshot.services.isEmpty { + Text(L("No CAAM environments configured.")).font(.caption) + } + ForEach(snapshot.services) { service in + HStack(alignment: .top) { + Text(service.row.label) + Spacer() + HubEvidenceView(evidence: service.evidence) + } + .font(.caption) + } + Divider() + Text(L("Provider usage and API cost")).font(.subheadline.weight(.semibold)) + if snapshot.providers.isEmpty { + Text(L("No provider observations are available.")).font(.caption) + } + ForEach(snapshot.providers) { provider in + VStack(alignment: .leading, spacing: 4) { + HStack { + Text(provider.label).font(.caption.weight(.semibold)) + Spacer() + if let percent = provider.primaryUsedPercent { + Text(String(format: L("Primary window: %.1f%% used"), percent)) + } else { + Text(L("Usage unavailable")) + } + } + if let minutes = provider.windowMinutes { + Text(String(format: L("Window length: %d minutes"), minutes)).font(.caption2) + } + if let reset = provider.resetsAt { + Text(String(format: L("Resets: %@"), reset.formatted())).font(.caption2) + } + HubEvidenceView(evidence: provider.evidence) + Text(String(format: L("Data confidence: %@"), provider.confidence.rawValue)).font(.caption2) + if let cost = provider.cost { + self.costRow(cost) + } else { + Text(L("Cost unavailable; not zero.")).font(.caption2).foregroundStyle(.secondary) + } + } + .font(.caption) + } + Divider() + Text(L("Host resources")).font(.subheadline.weight(.semibold)) + Text(L("Netdata telemetry is unsupported here. No host metrics were collected.")) + .font(.caption).foregroundStyle(.secondary) + } + } + + private func costRow(_ cost: HubCostObservation) -> some View { + VStack(alignment: .leading, spacing: 3) { + if let amount = cost.amount { + Text(String( + format: L("Recorded cost: %@ · %d days · %@"), + UsageFormatter.currencyString(amount, currencyCode: cost.currency), + cost.historyDays, + self.provenanceLabel(cost.provenance))) + } else { + Text(L("Cost unavailable; not zero.")) + } + if let metered = cost.meteredAmount { + Text(String( + format: L("Separate metered amount: %@ (not added)"), + UsageFormatter.currencyString(metered, currencyCode: cost.currency))) + } + Text(L("Historical provider cache; not a billing receipt or an identified CAAM account.")) + HubEvidenceView(evidence: cost.evidence) + } + .font(.caption2) + .foregroundStyle(.secondary) + } + + private func accounts(_ snapshot: ObservabilityHubSnapshot) -> some View { + Group { + Text(L("Profiles are environment-local. Only provider and stable ID establish cross-environment identity.")) + .font(.caption).foregroundStyle(.secondary) + if snapshot.accounts.isEmpty { + Text(L("No account profiles have been observed.")) + } + ForEach(snapshot.accounts) { account in + VStack(alignment: .leading, spacing: 3) { + Text("\(account.environmentLabel) · \(account.profile)").font(.caption.weight(.semibold)) + if let label = account.displayLabel { Text(label).font(.caption) } + Text(String( + format: L("Provider: %@ · Health: %@"), + account.provider ?? L("Unknown"), + account.health.rawValue)) + .font(.caption2) + if account.hostDefault { Text(L("Host default")).font(.caption2) } + HubEvidenceView(evidence: account.evidence) + } + } + } + } + + private func systems(_ snapshot: ObservabilityHubSnapshot) -> some View { + Group { + if snapshot.services.isEmpty { Text(L("No CAAM environments configured.")) } + ForEach(snapshot.services) { service in + VStack(alignment: .leading, spacing: 4) { + Text(service.row.label).font(.subheadline.weight(.semibold)) + Text(String(format: L("Host default: %@"), service.row.currentProfile ?? L("Unknown"))) + Text(String( + format: L("Runtime effective profile: %@"), + service.runtimeEffectiveProfile ?? L("Unknown"))) + HubEvidenceView(evidence: service.evidence) + Text(L("Netdata telemetry is unsupported here. No host metrics were collected.")) + .foregroundStyle(.secondary) + NetdataDashboardLinkView(configuration: self.configurations.first { $0.id == service.id }) + } + .font(.caption) + } + } + } + + private func work(_ snapshot: ObservabilityHubSnapshot) -> some View { + Group { + Text(L("Recorded local sessions only; not a live job queue or evidence of running processes.")) + .font(.caption).foregroundStyle(.secondary) + if snapshot.work.isEmpty { + Text(L("No session details are loaded. Use the existing provider refresh to collect supported data.")) + .font(.caption) + } + ForEach(snapshot.work) { work in + VStack(alignment: .leading, spacing: 3) { + Text(String(format: L("%@ · Recorded session %d"), work.provider, work.ordinal)) + .font(.caption.weight(.semibold)) + Text(work.lastActivity, style: .date).font(.caption2) + if let tokens = work.tokens { Text(String(format: L("Tokens: %d"), tokens)).font(.caption2) } + if let amount = work.estimatedCostUSD { + Text(String( + format: L("Estimated API cost: %@"), + UsageFormatter.currencyString(amount, currencyCode: "USD"))) + .font(.caption2) + } + HubEvidenceView(evidence: work.evidence) + } + } + if snapshot.workIsPartial { + Text(L("Partial view: limited to 100 recorded sessions.")).font(.caption).foregroundStyle(.secondary) + } + } + } + + private func provenanceLabel(_ provenance: CostProvenance) -> String { + switch provenance { + case .listPriceEstimate: L("List-price estimate") + case .vendorMetered: L("Vendor-metered") + case .mixed: L("Mixed provenance") + case .unknown: L("Unknown provenance") + } + } +} + +private struct HubEvidenceView: View { + let evidence: HubEvidence + + var body: some View { + VStack(alignment: .leading, spacing: 2) { + Text("\(self.evidence.source) · \(self.availabilityLabel)") + if let date = self.evidence.observedAt { + Text(String(format: L("Observed: %@"), date.formatted())) + } else { + Text(L("No observation timestamp")) + } + } + .font(.caption2) + .foregroundStyle(.secondary) + } + + private var availabilityLabel: String { + switch self.evidence.availability { + case .available: L("Available") + case .stale: L("Stale") + case .partial: L("Partial") + case .unavailable: L("Unavailable") + case .unsupported: L("Unsupported") + } + } +} + +private struct NetdataDashboardLinkView: View { + let configuration: CAAMEnvironmentConfiguration? + @State private var destination = "" + + var body: some View { + DisclosureGroup(L("Open an existing Netdata dashboard")) { + TextField(L("Dashboard URL (not stored)"), text: self.$destination) + .textFieldStyle(.roundedBorder) + if let url = NetdataReadOnlyFacade.dashboardURL(self.destination) { + Link(L("Open dashboard in browser"), destination: url) + } else if !self.destination.isEmpty { + Text(L("Use HTTPS, or loopback HTTP, without credentials, query parameters, or fragments.")) + .foregroundStyle(.orange) + } + Text(L("Opening a dashboard does not import telemetry or verify this host.")) + .foregroundStyle(.secondary) + } + .onChange(of: self.configuration) { _, _ in self.destination = "" } + } +} diff --git a/Sources/CodexBar/PreferencesCodexEnvironmentsSection.swift b/Sources/CodexBar/PreferencesCodexEnvironmentsSection.swift index 80760f692d..f438470dfd 100644 --- a/Sources/CodexBar/PreferencesCodexEnvironmentsSection.swift +++ b/Sources/CodexBar/PreferencesCodexEnvironmentsSection.swift @@ -16,6 +16,7 @@ struct CodexEnvironmentsSectionState: Equatable { @MainActor struct CodexEnvironmentsSectionView: View { let state: CodexEnvironmentsSectionState + let coordinator: CAAMEnvironmentCoordinator let saveConfigurations: ([CAAMEnvironmentConfiguration]) -> String? let refresh: () -> Void @@ -32,10 +33,28 @@ struct CodexEnvironmentsSectionView: View { .foregroundStyle(.secondary) } else { ForEach(self.state.rows) { row in - CodexEnvironmentRowView( - row: row, - canRemove: !self.state.isRefreshing, - remove: { self.removeEnvironment(id: row.id) }) + VStack(alignment: .leading, spacing: 8) { + CodexEnvironmentRowView( + row: row, + canRemove: self.coordinator.canChangeConfigurations( + from: self.state.configurations, + to: self.state.configurations.filter { $0.id != row.id }), + remove: { self.removeEnvironment(id: row.id) }) + if let configuration = self.state.configurations.first(where: { $0.id == row.id }) { + CAAMEnvironmentControlsView(configuration: configuration, coordinator: self.coordinator) + } + } + } + } + + ForEach(self.coordinator.retainedConfigurations(outside: self.state.configurations)) { configuration in + GroupBox(L("Retained unresolved operation")) { + VStack(alignment: .leading, spacing: 6) { + Text("\(configuration.label) · \(configuration.id)").font(.caption.weight(.semibold)) + Text(L("Status and recovery remain bound to the original environment configuration.")) + .font(.caption) + CAAMEnvironmentControlsView(configuration: configuration, coordinator: self.coordinator) + } } } @@ -177,6 +196,11 @@ private struct CodexEnvironmentRowView: View { .font(.caption) .foregroundStyle(.orange) } + if let runtime = self.row.runtimeState { + Text(String(format: L("Runtime state: %@"), runtime.rawValue)) + .font(.caption) + .foregroundStyle(.secondary) + } } Spacer(minLength: 8) @@ -220,6 +244,7 @@ private struct CodexEnvironmentRowView: View { case .incompatible: L("Update required") case .recoveryRequired: L("Recovery required") case .unavailable: L("Unavailable") + case .stale: L("Stale") } } @@ -227,7 +252,7 @@ private struct CodexEnvironmentRowView: View { switch self.row.availability { case .ready: .green case .degraded: .yellow - case .incompatible, .recoveryRequired, .unavailable: .orange + case .incompatible, .recoveryRequired, .unavailable, .stale: .orange } } } diff --git a/Sources/CodexBar/PreferencesProvidersPane+Observability.swift b/Sources/CodexBar/PreferencesProvidersPane+Observability.swift new file mode 100644 index 0000000000..622789a87b --- /dev/null +++ b/Sources/CodexBar/PreferencesProvidersPane+Observability.swift @@ -0,0 +1,29 @@ +import CodexBarCore + +extension ProvidersPane { + func observabilityProviderInputs() -> [HubProviderInput] { + self.store.enabledProvidersForDisplay().prefix(ObservabilityHub.maximumProviders).map { instanceID in + let provider = instanceID.firstPartyProvider + let publication = self.store.tokenSnapshotPublications[instanceID] + let cost: CostUsageTokenSnapshot? = if let provider, let publication, + publication.providerConfigRevision == + self.settings.providerConfigRevision(for: provider) + { + publication.snapshot + } else { + nil + } + // Deliberately do not call presentationSnapshot or scope-signature resolvers here: + // some of those helpers load disk-backed identity/cookie caches. Published cost records + // are historical provider-scoped observations, never attributed to a CAAM account. + return HubProviderInput( + id: instanceID.rawValue, + label: provider.map { self.store.metadata(for: $0).displayName } ?? instanceID.rawValue, + source: self.store.lastSourceLabels[instanceID] ?? "Existing provider snapshot", + usage: self.store.snapshots[instanceID], + cost: cost, + usageFailed: self.store.errors[instanceID] != nil, + costFailed: self.store.tokenErrors[instanceID] != nil) + } + } +} diff --git a/Sources/CodexBar/PreferencesProvidersPane.swift b/Sources/CodexBar/PreferencesProvidersPane.swift index b1fac3a669..7d633a7bb7 100644 --- a/Sources/CodexBar/PreferencesProvidersPane.swift +++ b/Sources/CodexBar/PreferencesProvidersPane.swift @@ -28,7 +28,7 @@ struct ProvidersPane: View { @State private var activeConfirmation: ProviderSettingsConfirmationState? @State private var codexAccountsNotice: CodexAccountsSectionNotice? @State private var isAuthenticatingLiveCodexAccount = false - @State private var caamEnvironmentCoordinator = CAAMEnvironmentCoordinator() + @State private var caamEnvironmentCoordinator: CAAMEnvironmentCoordinator init( // Provider-specific by design: Codex is the historical settings selection when no provider is supplied. @@ -38,6 +38,7 @@ struct ProvidersPane: View { managedCodexAccountCoordinator: ManagedCodexAccountCoordinator = ManagedCodexAccountCoordinator(), codexAccountPromotionCoordinator: CodexAccountPromotionCoordinator? = nil, codexAmbientLoginRunner: any CodexAmbientLoginRunning = DefaultCodexAmbientLoginRunner(), + caamEnvironmentCoordinator: CAAMEnvironmentCoordinator? = nil, runProviderLoginFlow: @escaping @MainActor (UsageProvider) async -> Void = { _ in }) { self.provider = provider @@ -50,6 +51,8 @@ struct ProvidersPane: View { usageStore: store, managedAccountCoordinator: managedCodexAccountCoordinator) self.codexAmbientLoginRunner = codexAmbientLoginRunner + self._caamEnvironmentCoordinator = State( + initialValue: caamEnvironmentCoordinator ?? CAAMEnvironmentCoordinator.applicationCoordinator()) self.runProviderLoginFlow = runProviderLoginFlow } @@ -101,17 +104,29 @@ struct ProvidersPane: View { await self.addManagedCodexAccount() } }) - CodexEnvironmentsSectionView( - state: self.codexEnvironmentsSectionState(), - saveConfigurations: { configurations in - self.saveCodexEnvironmentConfigurations(configurations) - }, - refresh: { - Task { @MainActor in - await self.caamEnvironmentCoordinator.refresh( - configurations: self.settings.codexCAAMEnvironments) - } - }) + TimelineView(.periodic(from: .now, by: 15)) { context in + CodexEnvironmentsSectionView( + state: self.caamEnvironmentCoordinator.state( + configurations: self.settings.codexCAAMEnvironments, at: context.date), + coordinator: self.caamEnvironmentCoordinator, + saveConfigurations: { configurations in + self.saveCodexEnvironmentConfigurations(configurations) + }, + refresh: { + Task { @MainActor in + await self.caamEnvironmentCoordinator.refresh( + configurations: self.settings.codexCAAMEnvironments) + } + }) + } + CodexObservabilityHubView( + coordinator: self.caamEnvironmentCoordinator, + configurations: self.settings.codexCAAMEnvironments, + providerInputs: { self.observabilityProviderInputs() }) + .task { + await self.caamEnvironmentCoordinator.refresh( + configurations: self.settings.codexCAAMEnvironments, force: false) + } } }) .onReceive(NotificationCenter.default.publisher(for: NSApplication.didBecomeActiveNotification)) { _ in @@ -266,6 +281,9 @@ struct ProvidersPane: View { } func saveCodexEnvironmentConfigurations(_ configurations: [CAAMEnvironmentConfiguration]) -> String? { + guard self.caamEnvironmentCoordinator.canChangeConfigurations( + from: self.settings.codexCAAMEnvironments, to: configurations) + else { return L("Refresh and resolve pending operations before changing this environment.") } do { try self.settings.setCodexCAAMEnvironments(configurations) self.caamEnvironmentCoordinator.configurationsDidChange(configurations) diff --git a/Sources/CodexBar/Resources/ar.lproj/Localizable.strings b/Sources/CodexBar/Resources/ar.lproj/Localizable.strings index b091d1c78f..3edd653973 100644 --- a/Sources/CodexBar/Resources/ar.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/ar.lproj/Localizable.strings @@ -1566,3 +1566,78 @@ "SSH" = "SSH"; "SSH destination" = "وجهة SSH"; "Update required" = "التحديث مطلوب"; + +/* CAAM observability: English fallback pending translation review. */ +"%@ · Recorded session %d" = "%@ · Recorded session %d"; +"Affected profiles" = "Affected profiles"; +"An unresolved operation belongs to a changed environment. Restore its original configuration." = "An unresolved operation belongs to a changed environment. Restore its original configuration."; +"Available" = "Available"; +"CAAM may complete or roll back its pending transaction. This does not restore fallback." = "CAAM may complete or roll back its pending transaction. This does not restore fallback."; +"CAAM operation tracking is unavailable. Mutations remain disabled." = "CAAM operation tracking is unavailable. Mutations remain disabled."; +"CAAM requires manual intervention on this environment. Automatic recovery is disabled." = "CAAM requires manual intervention on this environment. Automatic recovery is disabled."; +"Change host default: %@ → %@" = "Change host default: %@ → %@"; +"Confirm once" = "Confirm once"; +"Cost unavailable; not zero." = "Cost unavailable; not zero."; +"Dashboard URL (not stored)" = "Dashboard URL (not stored)"; +"Data confidence: %@" = "Data confidence: %@"; +"Estimated API cost: %@" = "Estimated API cost: %@"; +"Expected revision: %@" = "Expected revision: %@"; +"Gateway code: %@" = "Gateway code: %@"; +"Historical provider cache; not a billing receipt or an identified CAAM account." = "Historical provider cache; not a billing receipt or an identified CAAM account."; +"Host default" = "Host default"; +"Host default: %@" = "Host default: %@"; +"Host resources" = "Host resources"; +"Last operation: %@ · %@" = "Last operation: %@ · %@"; +"List-price estimate" = "List-price estimate"; +"Local service state, provider usage and cost, and host resources are separate ledgers." = "Local service state, provider usage and cost, and host resources are separate ledgers."; +"Look up operation" = "Look up operation"; +"Mixed provenance" = "Mixed provenance"; +"Netdata telemetry is unsupported here. No host metrics were collected." = "Netdata telemetry is unsupported here. No host metrics were collected."; +"No account profiles have been observed." = "No account profiles have been observed."; +"No observation timestamp" = "No observation timestamp"; +"No provider observations are available." = "No provider observations are available."; +"No session details are loaded. Use the existing provider refresh to collect supported data." = "No session details are loaded. Use the existing provider refresh to collect supported data."; +"Observability hub" = "Observability hub"; +"Observability view" = "Observability view"; +"Observed: %@" = "Observed: %@"; +"Open an existing Netdata dashboard" = "Open an existing Netdata dashboard"; +"Open dashboard in browser" = "Open dashboard in browser"; +"Opening a dashboard does not import telemetry or verify this host." = "Opening a dashboard does not import telemetry or verify this host."; +"Operation outcome is unresolved. Look up the existing operation before another change." = "Operation outcome is unresolved. Look up the existing operation before another change."; +"Partial" = "Partial"; +"Partial view: limited to 100 recorded sessions." = "Partial view: limited to 100 recorded sessions."; +"Plan digest: %@" = "Plan digest: %@"; +"Plan fallback" = "Plan fallback"; +"Plan switch" = "Plan switch"; +"Primary window: %.1f%% used" = "Primary window: %.1f%% used"; +"Profiles are environment-local. Only provider and stable ID establish cross-environment identity." = "Profiles are environment-local. Only provider and stable ID establish cross-environment identity."; +"Provider usage and API cost" = "Provider usage and API cost"; +"Provider: %@ · Health: %@" = "Provider: %@ · Health: %@"; +"Read-only until CAAM conformance is independently qualified." = "Read-only until CAAM conformance is independently qualified."; +"Recorded cost: %@ · %d days · %@" = "Recorded cost: %@ · %d days · %@"; +"Recorded local sessions only; not a live job queue or evidence of running processes." = "Recorded local sessions only; not a live job queue or evidence of running processes."; +"Refresh and resolve pending operations before changing this environment." = "Refresh and resolve pending operations before changing this environment."; +"Resets: %@" = "Resets: %@"; +"Resolve retained operations before adding more environment observations." = "Resolve retained operations before adding more environment observations."; +"Retained unresolved operation" = "Retained unresolved operation"; +"Review fallback restoration" = "Review fallback restoration"; +"Review interrupted-operation recovery" = "Review interrupted-operation recovery"; +"Review recovery" = "Review recovery"; +"Review switch plan" = "Review switch plan"; +"Runtime effective profile: %@" = "Runtime effective profile: %@"; +"Runtime state: %@" = "Runtime state: %@"; +"Separate metered amount: %@ (not added)" = "Separate metered amount: %@ (not added)"; +"Service and local state" = "Service and local state"; +"Stale" = "Stale"; +"Status and recovery remain bound to the original environment configuration." = "Status and recovery remain bound to the original environment configuration."; +"Systems" = "Systems"; +"The CAAM configuration is invalid." = "The CAAM configuration is invalid."; +"Tokens: %d" = "Tokens: %d"; +"Unknown provenance" = "Unknown provenance"; +"Unresolved operation: %@" = "Unresolved operation: %@"; +"Usage unavailable" = "Usage unavailable"; +"Use HTTPS, or loopback HTTP, without credentials, query parameters, or fragments." = "Use HTTPS, or loopback HTTP, without credentials, query parameters, or fragments."; +"Vendor-metered" = "Vendor-metered"; +"Waiting for CAAM; the app remains available." = "Waiting for CAAM; the app remains available."; +"Window length: %d minutes" = "Window length: %d minutes"; +"Work" = "Work"; diff --git a/Sources/CodexBar/Resources/ca.lproj/Localizable.strings b/Sources/CodexBar/Resources/ca.lproj/Localizable.strings index 1a73721e4e..6c1b0d28ab 100644 --- a/Sources/CodexBar/Resources/ca.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/ca.lproj/Localizable.strings @@ -1564,3 +1564,78 @@ "SSH" = "SSH"; "SSH destination" = "Destinació SSH"; "Update required" = "Cal actualitzar"; + +/* CAAM observability: English fallback pending translation review. */ +"%@ · Recorded session %d" = "%@ · Recorded session %d"; +"Affected profiles" = "Affected profiles"; +"An unresolved operation belongs to a changed environment. Restore its original configuration." = "An unresolved operation belongs to a changed environment. Restore its original configuration."; +"Available" = "Available"; +"CAAM may complete or roll back its pending transaction. This does not restore fallback." = "CAAM may complete or roll back its pending transaction. This does not restore fallback."; +"CAAM operation tracking is unavailable. Mutations remain disabled." = "CAAM operation tracking is unavailable. Mutations remain disabled."; +"CAAM requires manual intervention on this environment. Automatic recovery is disabled." = "CAAM requires manual intervention on this environment. Automatic recovery is disabled."; +"Change host default: %@ → %@" = "Change host default: %@ → %@"; +"Confirm once" = "Confirm once"; +"Cost unavailable; not zero." = "Cost unavailable; not zero."; +"Dashboard URL (not stored)" = "Dashboard URL (not stored)"; +"Data confidence: %@" = "Data confidence: %@"; +"Estimated API cost: %@" = "Estimated API cost: %@"; +"Expected revision: %@" = "Expected revision: %@"; +"Gateway code: %@" = "Gateway code: %@"; +"Historical provider cache; not a billing receipt or an identified CAAM account." = "Historical provider cache; not a billing receipt or an identified CAAM account."; +"Host default" = "Host default"; +"Host default: %@" = "Host default: %@"; +"Host resources" = "Host resources"; +"Last operation: %@ · %@" = "Last operation: %@ · %@"; +"List-price estimate" = "List-price estimate"; +"Local service state, provider usage and cost, and host resources are separate ledgers." = "Local service state, provider usage and cost, and host resources are separate ledgers."; +"Look up operation" = "Look up operation"; +"Mixed provenance" = "Mixed provenance"; +"Netdata telemetry is unsupported here. No host metrics were collected." = "Netdata telemetry is unsupported here. No host metrics were collected."; +"No account profiles have been observed." = "No account profiles have been observed."; +"No observation timestamp" = "No observation timestamp"; +"No provider observations are available." = "No provider observations are available."; +"No session details are loaded. Use the existing provider refresh to collect supported data." = "No session details are loaded. Use the existing provider refresh to collect supported data."; +"Observability hub" = "Observability hub"; +"Observability view" = "Observability view"; +"Observed: %@" = "Observed: %@"; +"Open an existing Netdata dashboard" = "Open an existing Netdata dashboard"; +"Open dashboard in browser" = "Open dashboard in browser"; +"Opening a dashboard does not import telemetry or verify this host." = "Opening a dashboard does not import telemetry or verify this host."; +"Operation outcome is unresolved. Look up the existing operation before another change." = "Operation outcome is unresolved. Look up the existing operation before another change."; +"Partial" = "Partial"; +"Partial view: limited to 100 recorded sessions." = "Partial view: limited to 100 recorded sessions."; +"Plan digest: %@" = "Plan digest: %@"; +"Plan fallback" = "Plan fallback"; +"Plan switch" = "Plan switch"; +"Primary window: %.1f%% used" = "Primary window: %.1f%% used"; +"Profiles are environment-local. Only provider and stable ID establish cross-environment identity." = "Profiles are environment-local. Only provider and stable ID establish cross-environment identity."; +"Provider usage and API cost" = "Provider usage and API cost"; +"Provider: %@ · Health: %@" = "Provider: %@ · Health: %@"; +"Read-only until CAAM conformance is independently qualified." = "Read-only until CAAM conformance is independently qualified."; +"Recorded cost: %@ · %d days · %@" = "Recorded cost: %@ · %d days · %@"; +"Recorded local sessions only; not a live job queue or evidence of running processes." = "Recorded local sessions only; not a live job queue or evidence of running processes."; +"Refresh and resolve pending operations before changing this environment." = "Refresh and resolve pending operations before changing this environment."; +"Resets: %@" = "Resets: %@"; +"Resolve retained operations before adding more environment observations." = "Resolve retained operations before adding more environment observations."; +"Retained unresolved operation" = "Retained unresolved operation"; +"Review fallback restoration" = "Review fallback restoration"; +"Review interrupted-operation recovery" = "Review interrupted-operation recovery"; +"Review recovery" = "Review recovery"; +"Review switch plan" = "Review switch plan"; +"Runtime effective profile: %@" = "Runtime effective profile: %@"; +"Runtime state: %@" = "Runtime state: %@"; +"Separate metered amount: %@ (not added)" = "Separate metered amount: %@ (not added)"; +"Service and local state" = "Service and local state"; +"Stale" = "Stale"; +"Status and recovery remain bound to the original environment configuration." = "Status and recovery remain bound to the original environment configuration."; +"Systems" = "Systems"; +"The CAAM configuration is invalid." = "The CAAM configuration is invalid."; +"Tokens: %d" = "Tokens: %d"; +"Unknown provenance" = "Unknown provenance"; +"Unresolved operation: %@" = "Unresolved operation: %@"; +"Usage unavailable" = "Usage unavailable"; +"Use HTTPS, or loopback HTTP, without credentials, query parameters, or fragments." = "Use HTTPS, or loopback HTTP, without credentials, query parameters, or fragments."; +"Vendor-metered" = "Vendor-metered"; +"Waiting for CAAM; the app remains available." = "Waiting for CAAM; the app remains available."; +"Window length: %d minutes" = "Window length: %d minutes"; +"Work" = "Work"; diff --git a/Sources/CodexBar/Resources/de.lproj/Localizable.strings b/Sources/CodexBar/Resources/de.lproj/Localizable.strings index aaf80bd020..c876da26c6 100644 --- a/Sources/CodexBar/Resources/de.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/de.lproj/Localizable.strings @@ -1562,3 +1562,78 @@ "SSH" = "SSH"; "SSH destination" = "SSH-Ziel"; "Update required" = "Aktualisierung erforderlich"; + +/* CAAM observability: English fallback pending translation review. */ +"%@ · Recorded session %d" = "%@ · Recorded session %d"; +"Affected profiles" = "Affected profiles"; +"An unresolved operation belongs to a changed environment. Restore its original configuration." = "An unresolved operation belongs to a changed environment. Restore its original configuration."; +"Available" = "Available"; +"CAAM may complete or roll back its pending transaction. This does not restore fallback." = "CAAM may complete or roll back its pending transaction. This does not restore fallback."; +"CAAM operation tracking is unavailable. Mutations remain disabled." = "CAAM operation tracking is unavailable. Mutations remain disabled."; +"CAAM requires manual intervention on this environment. Automatic recovery is disabled." = "CAAM requires manual intervention on this environment. Automatic recovery is disabled."; +"Change host default: %@ → %@" = "Change host default: %@ → %@"; +"Confirm once" = "Confirm once"; +"Cost unavailable; not zero." = "Cost unavailable; not zero."; +"Dashboard URL (not stored)" = "Dashboard URL (not stored)"; +"Data confidence: %@" = "Data confidence: %@"; +"Estimated API cost: %@" = "Estimated API cost: %@"; +"Expected revision: %@" = "Expected revision: %@"; +"Gateway code: %@" = "Gateway code: %@"; +"Historical provider cache; not a billing receipt or an identified CAAM account." = "Historical provider cache; not a billing receipt or an identified CAAM account."; +"Host default" = "Host default"; +"Host default: %@" = "Host default: %@"; +"Host resources" = "Host resources"; +"Last operation: %@ · %@" = "Last operation: %@ · %@"; +"List-price estimate" = "List-price estimate"; +"Local service state, provider usage and cost, and host resources are separate ledgers." = "Local service state, provider usage and cost, and host resources are separate ledgers."; +"Look up operation" = "Look up operation"; +"Mixed provenance" = "Mixed provenance"; +"Netdata telemetry is unsupported here. No host metrics were collected." = "Netdata telemetry is unsupported here. No host metrics were collected."; +"No account profiles have been observed." = "No account profiles have been observed."; +"No observation timestamp" = "No observation timestamp"; +"No provider observations are available." = "No provider observations are available."; +"No session details are loaded. Use the existing provider refresh to collect supported data." = "No session details are loaded. Use the existing provider refresh to collect supported data."; +"Observability hub" = "Observability hub"; +"Observability view" = "Observability view"; +"Observed: %@" = "Observed: %@"; +"Open an existing Netdata dashboard" = "Open an existing Netdata dashboard"; +"Open dashboard in browser" = "Open dashboard in browser"; +"Opening a dashboard does not import telemetry or verify this host." = "Opening a dashboard does not import telemetry or verify this host."; +"Operation outcome is unresolved. Look up the existing operation before another change." = "Operation outcome is unresolved. Look up the existing operation before another change."; +"Partial" = "Partial"; +"Partial view: limited to 100 recorded sessions." = "Partial view: limited to 100 recorded sessions."; +"Plan digest: %@" = "Plan digest: %@"; +"Plan fallback" = "Plan fallback"; +"Plan switch" = "Plan switch"; +"Primary window: %.1f%% used" = "Primary window: %.1f%% used"; +"Profiles are environment-local. Only provider and stable ID establish cross-environment identity." = "Profiles are environment-local. Only provider and stable ID establish cross-environment identity."; +"Provider usage and API cost" = "Provider usage and API cost"; +"Provider: %@ · Health: %@" = "Provider: %@ · Health: %@"; +"Read-only until CAAM conformance is independently qualified." = "Read-only until CAAM conformance is independently qualified."; +"Recorded cost: %@ · %d days · %@" = "Recorded cost: %@ · %d days · %@"; +"Recorded local sessions only; not a live job queue or evidence of running processes." = "Recorded local sessions only; not a live job queue or evidence of running processes."; +"Refresh and resolve pending operations before changing this environment." = "Refresh and resolve pending operations before changing this environment."; +"Resets: %@" = "Resets: %@"; +"Resolve retained operations before adding more environment observations." = "Resolve retained operations before adding more environment observations."; +"Retained unresolved operation" = "Retained unresolved operation"; +"Review fallback restoration" = "Review fallback restoration"; +"Review interrupted-operation recovery" = "Review interrupted-operation recovery"; +"Review recovery" = "Review recovery"; +"Review switch plan" = "Review switch plan"; +"Runtime effective profile: %@" = "Runtime effective profile: %@"; +"Runtime state: %@" = "Runtime state: %@"; +"Separate metered amount: %@ (not added)" = "Separate metered amount: %@ (not added)"; +"Service and local state" = "Service and local state"; +"Stale" = "Stale"; +"Status and recovery remain bound to the original environment configuration." = "Status and recovery remain bound to the original environment configuration."; +"Systems" = "Systems"; +"The CAAM configuration is invalid." = "The CAAM configuration is invalid."; +"Tokens: %d" = "Tokens: %d"; +"Unknown provenance" = "Unknown provenance"; +"Unresolved operation: %@" = "Unresolved operation: %@"; +"Usage unavailable" = "Usage unavailable"; +"Use HTTPS, or loopback HTTP, without credentials, query parameters, or fragments." = "Use HTTPS, or loopback HTTP, without credentials, query parameters, or fragments."; +"Vendor-metered" = "Vendor-metered"; +"Waiting for CAAM; the app remains available." = "Waiting for CAAM; the app remains available."; +"Window length: %d minutes" = "Window length: %d minutes"; +"Work" = "Work"; diff --git a/Sources/CodexBar/Resources/en.lproj/Localizable.strings b/Sources/CodexBar/Resources/en.lproj/Localizable.strings index e45f2d2ab9..0f56746e61 100644 --- a/Sources/CodexBar/Resources/en.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/en.lproj/Localizable.strings @@ -1565,3 +1565,78 @@ "SSH" = "SSH"; "SSH destination" = "SSH destination"; "Update required" = "Update required"; + +/* CAAM observability: English fallback pending translation review. */ +"%@ · Recorded session %d" = "%@ · Recorded session %d"; +"Affected profiles" = "Affected profiles"; +"An unresolved operation belongs to a changed environment. Restore its original configuration." = "An unresolved operation belongs to a changed environment. Restore its original configuration."; +"Available" = "Available"; +"CAAM may complete or roll back its pending transaction. This does not restore fallback." = "CAAM may complete or roll back its pending transaction. This does not restore fallback."; +"CAAM operation tracking is unavailable. Mutations remain disabled." = "CAAM operation tracking is unavailable. Mutations remain disabled."; +"CAAM requires manual intervention on this environment. Automatic recovery is disabled." = "CAAM requires manual intervention on this environment. Automatic recovery is disabled."; +"Change host default: %@ → %@" = "Change host default: %@ → %@"; +"Confirm once" = "Confirm once"; +"Cost unavailable; not zero." = "Cost unavailable; not zero."; +"Dashboard URL (not stored)" = "Dashboard URL (not stored)"; +"Data confidence: %@" = "Data confidence: %@"; +"Estimated API cost: %@" = "Estimated API cost: %@"; +"Expected revision: %@" = "Expected revision: %@"; +"Gateway code: %@" = "Gateway code: %@"; +"Historical provider cache; not a billing receipt or an identified CAAM account." = "Historical provider cache; not a billing receipt or an identified CAAM account."; +"Host default" = "Host default"; +"Host default: %@" = "Host default: %@"; +"Host resources" = "Host resources"; +"Last operation: %@ · %@" = "Last operation: %@ · %@"; +"List-price estimate" = "List-price estimate"; +"Local service state, provider usage and cost, and host resources are separate ledgers." = "Local service state, provider usage and cost, and host resources are separate ledgers."; +"Look up operation" = "Look up operation"; +"Mixed provenance" = "Mixed provenance"; +"Netdata telemetry is unsupported here. No host metrics were collected." = "Netdata telemetry is unsupported here. No host metrics were collected."; +"No account profiles have been observed." = "No account profiles have been observed."; +"No observation timestamp" = "No observation timestamp"; +"No provider observations are available." = "No provider observations are available."; +"No session details are loaded. Use the existing provider refresh to collect supported data." = "No session details are loaded. Use the existing provider refresh to collect supported data."; +"Observability hub" = "Observability hub"; +"Observability view" = "Observability view"; +"Observed: %@" = "Observed: %@"; +"Open an existing Netdata dashboard" = "Open an existing Netdata dashboard"; +"Open dashboard in browser" = "Open dashboard in browser"; +"Opening a dashboard does not import telemetry or verify this host." = "Opening a dashboard does not import telemetry or verify this host."; +"Operation outcome is unresolved. Look up the existing operation before another change." = "Operation outcome is unresolved. Look up the existing operation before another change."; +"Partial" = "Partial"; +"Partial view: limited to 100 recorded sessions." = "Partial view: limited to 100 recorded sessions."; +"Plan digest: %@" = "Plan digest: %@"; +"Plan fallback" = "Plan fallback"; +"Plan switch" = "Plan switch"; +"Primary window: %.1f%% used" = "Primary window: %.1f%% used"; +"Profiles are environment-local. Only provider and stable ID establish cross-environment identity." = "Profiles are environment-local. Only provider and stable ID establish cross-environment identity."; +"Provider usage and API cost" = "Provider usage and API cost"; +"Provider: %@ · Health: %@" = "Provider: %@ · Health: %@"; +"Read-only until CAAM conformance is independently qualified." = "Read-only until CAAM conformance is independently qualified."; +"Recorded cost: %@ · %d days · %@" = "Recorded cost: %@ · %d days · %@"; +"Recorded local sessions only; not a live job queue or evidence of running processes." = "Recorded local sessions only; not a live job queue or evidence of running processes."; +"Refresh and resolve pending operations before changing this environment." = "Refresh and resolve pending operations before changing this environment."; +"Resets: %@" = "Resets: %@"; +"Resolve retained operations before adding more environment observations." = "Resolve retained operations before adding more environment observations."; +"Retained unresolved operation" = "Retained unresolved operation"; +"Review fallback restoration" = "Review fallback restoration"; +"Review interrupted-operation recovery" = "Review interrupted-operation recovery"; +"Review recovery" = "Review recovery"; +"Review switch plan" = "Review switch plan"; +"Runtime effective profile: %@" = "Runtime effective profile: %@"; +"Runtime state: %@" = "Runtime state: %@"; +"Separate metered amount: %@ (not added)" = "Separate metered amount: %@ (not added)"; +"Service and local state" = "Service and local state"; +"Stale" = "Stale"; +"Status and recovery remain bound to the original environment configuration." = "Status and recovery remain bound to the original environment configuration."; +"Systems" = "Systems"; +"The CAAM configuration is invalid." = "The CAAM configuration is invalid."; +"Tokens: %d" = "Tokens: %d"; +"Unknown provenance" = "Unknown provenance"; +"Unresolved operation: %@" = "Unresolved operation: %@"; +"Usage unavailable" = "Usage unavailable"; +"Use HTTPS, or loopback HTTP, without credentials, query parameters, or fragments." = "Use HTTPS, or loopback HTTP, without credentials, query parameters, or fragments."; +"Vendor-metered" = "Vendor-metered"; +"Waiting for CAAM; the app remains available." = "Waiting for CAAM; the app remains available."; +"Window length: %d minutes" = "Window length: %d minutes"; +"Work" = "Work"; diff --git a/Sources/CodexBar/Resources/es.lproj/Localizable.strings b/Sources/CodexBar/Resources/es.lproj/Localizable.strings index 9d59a65de7..6cbedca021 100644 --- a/Sources/CodexBar/Resources/es.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/es.lproj/Localizable.strings @@ -1560,3 +1560,78 @@ "SSH" = "SSH"; "SSH destination" = "Destino SSH"; "Update required" = "Se requiere una actualización"; + +/* CAAM observability: English fallback pending translation review. */ +"%@ · Recorded session %d" = "%@ · Recorded session %d"; +"Affected profiles" = "Affected profiles"; +"An unresolved operation belongs to a changed environment. Restore its original configuration." = "An unresolved operation belongs to a changed environment. Restore its original configuration."; +"Available" = "Available"; +"CAAM may complete or roll back its pending transaction. This does not restore fallback." = "CAAM may complete or roll back its pending transaction. This does not restore fallback."; +"CAAM operation tracking is unavailable. Mutations remain disabled." = "CAAM operation tracking is unavailable. Mutations remain disabled."; +"CAAM requires manual intervention on this environment. Automatic recovery is disabled." = "CAAM requires manual intervention on this environment. Automatic recovery is disabled."; +"Change host default: %@ → %@" = "Change host default: %@ → %@"; +"Confirm once" = "Confirm once"; +"Cost unavailable; not zero." = "Cost unavailable; not zero."; +"Dashboard URL (not stored)" = "Dashboard URL (not stored)"; +"Data confidence: %@" = "Data confidence: %@"; +"Estimated API cost: %@" = "Estimated API cost: %@"; +"Expected revision: %@" = "Expected revision: %@"; +"Gateway code: %@" = "Gateway code: %@"; +"Historical provider cache; not a billing receipt or an identified CAAM account." = "Historical provider cache; not a billing receipt or an identified CAAM account."; +"Host default" = "Host default"; +"Host default: %@" = "Host default: %@"; +"Host resources" = "Host resources"; +"Last operation: %@ · %@" = "Last operation: %@ · %@"; +"List-price estimate" = "List-price estimate"; +"Local service state, provider usage and cost, and host resources are separate ledgers." = "Local service state, provider usage and cost, and host resources are separate ledgers."; +"Look up operation" = "Look up operation"; +"Mixed provenance" = "Mixed provenance"; +"Netdata telemetry is unsupported here. No host metrics were collected." = "Netdata telemetry is unsupported here. No host metrics were collected."; +"No account profiles have been observed." = "No account profiles have been observed."; +"No observation timestamp" = "No observation timestamp"; +"No provider observations are available." = "No provider observations are available."; +"No session details are loaded. Use the existing provider refresh to collect supported data." = "No session details are loaded. Use the existing provider refresh to collect supported data."; +"Observability hub" = "Observability hub"; +"Observability view" = "Observability view"; +"Observed: %@" = "Observed: %@"; +"Open an existing Netdata dashboard" = "Open an existing Netdata dashboard"; +"Open dashboard in browser" = "Open dashboard in browser"; +"Opening a dashboard does not import telemetry or verify this host." = "Opening a dashboard does not import telemetry or verify this host."; +"Operation outcome is unresolved. Look up the existing operation before another change." = "Operation outcome is unresolved. Look up the existing operation before another change."; +"Partial" = "Partial"; +"Partial view: limited to 100 recorded sessions." = "Partial view: limited to 100 recorded sessions."; +"Plan digest: %@" = "Plan digest: %@"; +"Plan fallback" = "Plan fallback"; +"Plan switch" = "Plan switch"; +"Primary window: %.1f%% used" = "Primary window: %.1f%% used"; +"Profiles are environment-local. Only provider and stable ID establish cross-environment identity." = "Profiles are environment-local. Only provider and stable ID establish cross-environment identity."; +"Provider usage and API cost" = "Provider usage and API cost"; +"Provider: %@ · Health: %@" = "Provider: %@ · Health: %@"; +"Read-only until CAAM conformance is independently qualified." = "Read-only until CAAM conformance is independently qualified."; +"Recorded cost: %@ · %d days · %@" = "Recorded cost: %@ · %d days · %@"; +"Recorded local sessions only; not a live job queue or evidence of running processes." = "Recorded local sessions only; not a live job queue or evidence of running processes."; +"Refresh and resolve pending operations before changing this environment." = "Refresh and resolve pending operations before changing this environment."; +"Resets: %@" = "Resets: %@"; +"Resolve retained operations before adding more environment observations." = "Resolve retained operations before adding more environment observations."; +"Retained unresolved operation" = "Retained unresolved operation"; +"Review fallback restoration" = "Review fallback restoration"; +"Review interrupted-operation recovery" = "Review interrupted-operation recovery"; +"Review recovery" = "Review recovery"; +"Review switch plan" = "Review switch plan"; +"Runtime effective profile: %@" = "Runtime effective profile: %@"; +"Runtime state: %@" = "Runtime state: %@"; +"Separate metered amount: %@ (not added)" = "Separate metered amount: %@ (not added)"; +"Service and local state" = "Service and local state"; +"Stale" = "Stale"; +"Status and recovery remain bound to the original environment configuration." = "Status and recovery remain bound to the original environment configuration."; +"Systems" = "Systems"; +"The CAAM configuration is invalid." = "The CAAM configuration is invalid."; +"Tokens: %d" = "Tokens: %d"; +"Unknown provenance" = "Unknown provenance"; +"Unresolved operation: %@" = "Unresolved operation: %@"; +"Usage unavailable" = "Usage unavailable"; +"Use HTTPS, or loopback HTTP, without credentials, query parameters, or fragments." = "Use HTTPS, or loopback HTTP, without credentials, query parameters, or fragments."; +"Vendor-metered" = "Vendor-metered"; +"Waiting for CAAM; the app remains available." = "Waiting for CAAM; the app remains available."; +"Window length: %d minutes" = "Window length: %d minutes"; +"Work" = "Work"; diff --git a/Sources/CodexBar/Resources/fa.lproj/Localizable.strings b/Sources/CodexBar/Resources/fa.lproj/Localizable.strings index b55412044a..1af685b3e8 100644 --- a/Sources/CodexBar/Resources/fa.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/fa.lproj/Localizable.strings @@ -1566,3 +1566,78 @@ "SSH" = "SSH"; "SSH destination" = "مقصد SSH"; "Update required" = "به‌روزرسانی لازم است"; + +/* CAAM observability: English fallback pending translation review. */ +"%@ · Recorded session %d" = "%@ · Recorded session %d"; +"Affected profiles" = "Affected profiles"; +"An unresolved operation belongs to a changed environment. Restore its original configuration." = "An unresolved operation belongs to a changed environment. Restore its original configuration."; +"Available" = "Available"; +"CAAM may complete or roll back its pending transaction. This does not restore fallback." = "CAAM may complete or roll back its pending transaction. This does not restore fallback."; +"CAAM operation tracking is unavailable. Mutations remain disabled." = "CAAM operation tracking is unavailable. Mutations remain disabled."; +"CAAM requires manual intervention on this environment. Automatic recovery is disabled." = "CAAM requires manual intervention on this environment. Automatic recovery is disabled."; +"Change host default: %@ → %@" = "Change host default: %@ → %@"; +"Confirm once" = "Confirm once"; +"Cost unavailable; not zero." = "Cost unavailable; not zero."; +"Dashboard URL (not stored)" = "Dashboard URL (not stored)"; +"Data confidence: %@" = "Data confidence: %@"; +"Estimated API cost: %@" = "Estimated API cost: %@"; +"Expected revision: %@" = "Expected revision: %@"; +"Gateway code: %@" = "Gateway code: %@"; +"Historical provider cache; not a billing receipt or an identified CAAM account." = "Historical provider cache; not a billing receipt or an identified CAAM account."; +"Host default" = "Host default"; +"Host default: %@" = "Host default: %@"; +"Host resources" = "Host resources"; +"Last operation: %@ · %@" = "Last operation: %@ · %@"; +"List-price estimate" = "List-price estimate"; +"Local service state, provider usage and cost, and host resources are separate ledgers." = "Local service state, provider usage and cost, and host resources are separate ledgers."; +"Look up operation" = "Look up operation"; +"Mixed provenance" = "Mixed provenance"; +"Netdata telemetry is unsupported here. No host metrics were collected." = "Netdata telemetry is unsupported here. No host metrics were collected."; +"No account profiles have been observed." = "No account profiles have been observed."; +"No observation timestamp" = "No observation timestamp"; +"No provider observations are available." = "No provider observations are available."; +"No session details are loaded. Use the existing provider refresh to collect supported data." = "No session details are loaded. Use the existing provider refresh to collect supported data."; +"Observability hub" = "Observability hub"; +"Observability view" = "Observability view"; +"Observed: %@" = "Observed: %@"; +"Open an existing Netdata dashboard" = "Open an existing Netdata dashboard"; +"Open dashboard in browser" = "Open dashboard in browser"; +"Opening a dashboard does not import telemetry or verify this host." = "Opening a dashboard does not import telemetry or verify this host."; +"Operation outcome is unresolved. Look up the existing operation before another change." = "Operation outcome is unresolved. Look up the existing operation before another change."; +"Partial" = "Partial"; +"Partial view: limited to 100 recorded sessions." = "Partial view: limited to 100 recorded sessions."; +"Plan digest: %@" = "Plan digest: %@"; +"Plan fallback" = "Plan fallback"; +"Plan switch" = "Plan switch"; +"Primary window: %.1f%% used" = "Primary window: %.1f%% used"; +"Profiles are environment-local. Only provider and stable ID establish cross-environment identity." = "Profiles are environment-local. Only provider and stable ID establish cross-environment identity."; +"Provider usage and API cost" = "Provider usage and API cost"; +"Provider: %@ · Health: %@" = "Provider: %@ · Health: %@"; +"Read-only until CAAM conformance is independently qualified." = "Read-only until CAAM conformance is independently qualified."; +"Recorded cost: %@ · %d days · %@" = "Recorded cost: %@ · %d days · %@"; +"Recorded local sessions only; not a live job queue or evidence of running processes." = "Recorded local sessions only; not a live job queue or evidence of running processes."; +"Refresh and resolve pending operations before changing this environment." = "Refresh and resolve pending operations before changing this environment."; +"Resets: %@" = "Resets: %@"; +"Resolve retained operations before adding more environment observations." = "Resolve retained operations before adding more environment observations."; +"Retained unresolved operation" = "Retained unresolved operation"; +"Review fallback restoration" = "Review fallback restoration"; +"Review interrupted-operation recovery" = "Review interrupted-operation recovery"; +"Review recovery" = "Review recovery"; +"Review switch plan" = "Review switch plan"; +"Runtime effective profile: %@" = "Runtime effective profile: %@"; +"Runtime state: %@" = "Runtime state: %@"; +"Separate metered amount: %@ (not added)" = "Separate metered amount: %@ (not added)"; +"Service and local state" = "Service and local state"; +"Stale" = "Stale"; +"Status and recovery remain bound to the original environment configuration." = "Status and recovery remain bound to the original environment configuration."; +"Systems" = "Systems"; +"The CAAM configuration is invalid." = "The CAAM configuration is invalid."; +"Tokens: %d" = "Tokens: %d"; +"Unknown provenance" = "Unknown provenance"; +"Unresolved operation: %@" = "Unresolved operation: %@"; +"Usage unavailable" = "Usage unavailable"; +"Use HTTPS, or loopback HTTP, without credentials, query parameters, or fragments." = "Use HTTPS, or loopback HTTP, without credentials, query parameters, or fragments."; +"Vendor-metered" = "Vendor-metered"; +"Waiting for CAAM; the app remains available." = "Waiting for CAAM; the app remains available."; +"Window length: %d minutes" = "Window length: %d minutes"; +"Work" = "Work"; diff --git a/Sources/CodexBar/Resources/fr.lproj/Localizable.strings b/Sources/CodexBar/Resources/fr.lproj/Localizable.strings index 03a04f8bc4..3d89328709 100644 --- a/Sources/CodexBar/Resources/fr.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/fr.lproj/Localizable.strings @@ -1561,3 +1561,78 @@ "SSH" = "SSH"; "SSH destination" = "Destination SSH"; "Update required" = "Mise à jour requise"; + +/* CAAM observability: English fallback pending translation review. */ +"%@ · Recorded session %d" = "%@ · Recorded session %d"; +"Affected profiles" = "Affected profiles"; +"An unresolved operation belongs to a changed environment. Restore its original configuration." = "An unresolved operation belongs to a changed environment. Restore its original configuration."; +"Available" = "Available"; +"CAAM may complete or roll back its pending transaction. This does not restore fallback." = "CAAM may complete or roll back its pending transaction. This does not restore fallback."; +"CAAM operation tracking is unavailable. Mutations remain disabled." = "CAAM operation tracking is unavailable. Mutations remain disabled."; +"CAAM requires manual intervention on this environment. Automatic recovery is disabled." = "CAAM requires manual intervention on this environment. Automatic recovery is disabled."; +"Change host default: %@ → %@" = "Change host default: %@ → %@"; +"Confirm once" = "Confirm once"; +"Cost unavailable; not zero." = "Cost unavailable; not zero."; +"Dashboard URL (not stored)" = "Dashboard URL (not stored)"; +"Data confidence: %@" = "Data confidence: %@"; +"Estimated API cost: %@" = "Estimated API cost: %@"; +"Expected revision: %@" = "Expected revision: %@"; +"Gateway code: %@" = "Gateway code: %@"; +"Historical provider cache; not a billing receipt or an identified CAAM account." = "Historical provider cache; not a billing receipt or an identified CAAM account."; +"Host default" = "Host default"; +"Host default: %@" = "Host default: %@"; +"Host resources" = "Host resources"; +"Last operation: %@ · %@" = "Last operation: %@ · %@"; +"List-price estimate" = "List-price estimate"; +"Local service state, provider usage and cost, and host resources are separate ledgers." = "Local service state, provider usage and cost, and host resources are separate ledgers."; +"Look up operation" = "Look up operation"; +"Mixed provenance" = "Mixed provenance"; +"Netdata telemetry is unsupported here. No host metrics were collected." = "Netdata telemetry is unsupported here. No host metrics were collected."; +"No account profiles have been observed." = "No account profiles have been observed."; +"No observation timestamp" = "No observation timestamp"; +"No provider observations are available." = "No provider observations are available."; +"No session details are loaded. Use the existing provider refresh to collect supported data." = "No session details are loaded. Use the existing provider refresh to collect supported data."; +"Observability hub" = "Observability hub"; +"Observability view" = "Observability view"; +"Observed: %@" = "Observed: %@"; +"Open an existing Netdata dashboard" = "Open an existing Netdata dashboard"; +"Open dashboard in browser" = "Open dashboard in browser"; +"Opening a dashboard does not import telemetry or verify this host." = "Opening a dashboard does not import telemetry or verify this host."; +"Operation outcome is unresolved. Look up the existing operation before another change." = "Operation outcome is unresolved. Look up the existing operation before another change."; +"Partial" = "Partial"; +"Partial view: limited to 100 recorded sessions." = "Partial view: limited to 100 recorded sessions."; +"Plan digest: %@" = "Plan digest: %@"; +"Plan fallback" = "Plan fallback"; +"Plan switch" = "Plan switch"; +"Primary window: %.1f%% used" = "Primary window: %.1f%% used"; +"Profiles are environment-local. Only provider and stable ID establish cross-environment identity." = "Profiles are environment-local. Only provider and stable ID establish cross-environment identity."; +"Provider usage and API cost" = "Provider usage and API cost"; +"Provider: %@ · Health: %@" = "Provider: %@ · Health: %@"; +"Read-only until CAAM conformance is independently qualified." = "Read-only until CAAM conformance is independently qualified."; +"Recorded cost: %@ · %d days · %@" = "Recorded cost: %@ · %d days · %@"; +"Recorded local sessions only; not a live job queue or evidence of running processes." = "Recorded local sessions only; not a live job queue or evidence of running processes."; +"Refresh and resolve pending operations before changing this environment." = "Refresh and resolve pending operations before changing this environment."; +"Resets: %@" = "Resets: %@"; +"Resolve retained operations before adding more environment observations." = "Resolve retained operations before adding more environment observations."; +"Retained unresolved operation" = "Retained unresolved operation"; +"Review fallback restoration" = "Review fallback restoration"; +"Review interrupted-operation recovery" = "Review interrupted-operation recovery"; +"Review recovery" = "Review recovery"; +"Review switch plan" = "Review switch plan"; +"Runtime effective profile: %@" = "Runtime effective profile: %@"; +"Runtime state: %@" = "Runtime state: %@"; +"Separate metered amount: %@ (not added)" = "Separate metered amount: %@ (not added)"; +"Service and local state" = "Service and local state"; +"Stale" = "Stale"; +"Status and recovery remain bound to the original environment configuration." = "Status and recovery remain bound to the original environment configuration."; +"Systems" = "Systems"; +"The CAAM configuration is invalid." = "The CAAM configuration is invalid."; +"Tokens: %d" = "Tokens: %d"; +"Unknown provenance" = "Unknown provenance"; +"Unresolved operation: %@" = "Unresolved operation: %@"; +"Usage unavailable" = "Usage unavailable"; +"Use HTTPS, or loopback HTTP, without credentials, query parameters, or fragments." = "Use HTTPS, or loopback HTTP, without credentials, query parameters, or fragments."; +"Vendor-metered" = "Vendor-metered"; +"Waiting for CAAM; the app remains available." = "Waiting for CAAM; the app remains available."; +"Window length: %d minutes" = "Window length: %d minutes"; +"Work" = "Work"; diff --git a/Sources/CodexBar/Resources/gl.lproj/Localizable.strings b/Sources/CodexBar/Resources/gl.lproj/Localizable.strings index 26e6b23196..af57d0438d 100644 --- a/Sources/CodexBar/Resources/gl.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/gl.lproj/Localizable.strings @@ -1561,3 +1561,78 @@ "SSH" = "SSH"; "SSH destination" = "Destino SSH"; "Update required" = "Requírese unha actualización"; + +/* CAAM observability: English fallback pending translation review. */ +"%@ · Recorded session %d" = "%@ · Recorded session %d"; +"Affected profiles" = "Affected profiles"; +"An unresolved operation belongs to a changed environment. Restore its original configuration." = "An unresolved operation belongs to a changed environment. Restore its original configuration."; +"Available" = "Available"; +"CAAM may complete or roll back its pending transaction. This does not restore fallback." = "CAAM may complete or roll back its pending transaction. This does not restore fallback."; +"CAAM operation tracking is unavailable. Mutations remain disabled." = "CAAM operation tracking is unavailable. Mutations remain disabled."; +"CAAM requires manual intervention on this environment. Automatic recovery is disabled." = "CAAM requires manual intervention on this environment. Automatic recovery is disabled."; +"Change host default: %@ → %@" = "Change host default: %@ → %@"; +"Confirm once" = "Confirm once"; +"Cost unavailable; not zero." = "Cost unavailable; not zero."; +"Dashboard URL (not stored)" = "Dashboard URL (not stored)"; +"Data confidence: %@" = "Data confidence: %@"; +"Estimated API cost: %@" = "Estimated API cost: %@"; +"Expected revision: %@" = "Expected revision: %@"; +"Gateway code: %@" = "Gateway code: %@"; +"Historical provider cache; not a billing receipt or an identified CAAM account." = "Historical provider cache; not a billing receipt or an identified CAAM account."; +"Host default" = "Host default"; +"Host default: %@" = "Host default: %@"; +"Host resources" = "Host resources"; +"Last operation: %@ · %@" = "Last operation: %@ · %@"; +"List-price estimate" = "List-price estimate"; +"Local service state, provider usage and cost, and host resources are separate ledgers." = "Local service state, provider usage and cost, and host resources are separate ledgers."; +"Look up operation" = "Look up operation"; +"Mixed provenance" = "Mixed provenance"; +"Netdata telemetry is unsupported here. No host metrics were collected." = "Netdata telemetry is unsupported here. No host metrics were collected."; +"No account profiles have been observed." = "No account profiles have been observed."; +"No observation timestamp" = "No observation timestamp"; +"No provider observations are available." = "No provider observations are available."; +"No session details are loaded. Use the existing provider refresh to collect supported data." = "No session details are loaded. Use the existing provider refresh to collect supported data."; +"Observability hub" = "Observability hub"; +"Observability view" = "Observability view"; +"Observed: %@" = "Observed: %@"; +"Open an existing Netdata dashboard" = "Open an existing Netdata dashboard"; +"Open dashboard in browser" = "Open dashboard in browser"; +"Opening a dashboard does not import telemetry or verify this host." = "Opening a dashboard does not import telemetry or verify this host."; +"Operation outcome is unresolved. Look up the existing operation before another change." = "Operation outcome is unresolved. Look up the existing operation before another change."; +"Partial" = "Partial"; +"Partial view: limited to 100 recorded sessions." = "Partial view: limited to 100 recorded sessions."; +"Plan digest: %@" = "Plan digest: %@"; +"Plan fallback" = "Plan fallback"; +"Plan switch" = "Plan switch"; +"Primary window: %.1f%% used" = "Primary window: %.1f%% used"; +"Profiles are environment-local. Only provider and stable ID establish cross-environment identity." = "Profiles are environment-local. Only provider and stable ID establish cross-environment identity."; +"Provider usage and API cost" = "Provider usage and API cost"; +"Provider: %@ · Health: %@" = "Provider: %@ · Health: %@"; +"Read-only until CAAM conformance is independently qualified." = "Read-only until CAAM conformance is independently qualified."; +"Recorded cost: %@ · %d days · %@" = "Recorded cost: %@ · %d days · %@"; +"Recorded local sessions only; not a live job queue or evidence of running processes." = "Recorded local sessions only; not a live job queue or evidence of running processes."; +"Refresh and resolve pending operations before changing this environment." = "Refresh and resolve pending operations before changing this environment."; +"Resets: %@" = "Resets: %@"; +"Resolve retained operations before adding more environment observations." = "Resolve retained operations before adding more environment observations."; +"Retained unresolved operation" = "Retained unresolved operation"; +"Review fallback restoration" = "Review fallback restoration"; +"Review interrupted-operation recovery" = "Review interrupted-operation recovery"; +"Review recovery" = "Review recovery"; +"Review switch plan" = "Review switch plan"; +"Runtime effective profile: %@" = "Runtime effective profile: %@"; +"Runtime state: %@" = "Runtime state: %@"; +"Separate metered amount: %@ (not added)" = "Separate metered amount: %@ (not added)"; +"Service and local state" = "Service and local state"; +"Stale" = "Stale"; +"Status and recovery remain bound to the original environment configuration." = "Status and recovery remain bound to the original environment configuration."; +"Systems" = "Systems"; +"The CAAM configuration is invalid." = "The CAAM configuration is invalid."; +"Tokens: %d" = "Tokens: %d"; +"Unknown provenance" = "Unknown provenance"; +"Unresolved operation: %@" = "Unresolved operation: %@"; +"Usage unavailable" = "Usage unavailable"; +"Use HTTPS, or loopback HTTP, without credentials, query parameters, or fragments." = "Use HTTPS, or loopback HTTP, without credentials, query parameters, or fragments."; +"Vendor-metered" = "Vendor-metered"; +"Waiting for CAAM; the app remains available." = "Waiting for CAAM; the app remains available."; +"Window length: %d minutes" = "Window length: %d minutes"; +"Work" = "Work"; diff --git a/Sources/CodexBar/Resources/id.lproj/Localizable.strings b/Sources/CodexBar/Resources/id.lproj/Localizable.strings index c646a67f28..7ba8455f1a 100644 --- a/Sources/CodexBar/Resources/id.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/id.lproj/Localizable.strings @@ -1565,3 +1565,78 @@ "SSH" = "SSH"; "SSH destination" = "Tujuan SSH"; "Update required" = "Pembaruan diperlukan"; + +/* CAAM observability: English fallback pending translation review. */ +"%@ · Recorded session %d" = "%@ · Recorded session %d"; +"Affected profiles" = "Affected profiles"; +"An unresolved operation belongs to a changed environment. Restore its original configuration." = "An unresolved operation belongs to a changed environment. Restore its original configuration."; +"Available" = "Available"; +"CAAM may complete or roll back its pending transaction. This does not restore fallback." = "CAAM may complete or roll back its pending transaction. This does not restore fallback."; +"CAAM operation tracking is unavailable. Mutations remain disabled." = "CAAM operation tracking is unavailable. Mutations remain disabled."; +"CAAM requires manual intervention on this environment. Automatic recovery is disabled." = "CAAM requires manual intervention on this environment. Automatic recovery is disabled."; +"Change host default: %@ → %@" = "Change host default: %@ → %@"; +"Confirm once" = "Confirm once"; +"Cost unavailable; not zero." = "Cost unavailable; not zero."; +"Dashboard URL (not stored)" = "Dashboard URL (not stored)"; +"Data confidence: %@" = "Data confidence: %@"; +"Estimated API cost: %@" = "Estimated API cost: %@"; +"Expected revision: %@" = "Expected revision: %@"; +"Gateway code: %@" = "Gateway code: %@"; +"Historical provider cache; not a billing receipt or an identified CAAM account." = "Historical provider cache; not a billing receipt or an identified CAAM account."; +"Host default" = "Host default"; +"Host default: %@" = "Host default: %@"; +"Host resources" = "Host resources"; +"Last operation: %@ · %@" = "Last operation: %@ · %@"; +"List-price estimate" = "List-price estimate"; +"Local service state, provider usage and cost, and host resources are separate ledgers." = "Local service state, provider usage and cost, and host resources are separate ledgers."; +"Look up operation" = "Look up operation"; +"Mixed provenance" = "Mixed provenance"; +"Netdata telemetry is unsupported here. No host metrics were collected." = "Netdata telemetry is unsupported here. No host metrics were collected."; +"No account profiles have been observed." = "No account profiles have been observed."; +"No observation timestamp" = "No observation timestamp"; +"No provider observations are available." = "No provider observations are available."; +"No session details are loaded. Use the existing provider refresh to collect supported data." = "No session details are loaded. Use the existing provider refresh to collect supported data."; +"Observability hub" = "Observability hub"; +"Observability view" = "Observability view"; +"Observed: %@" = "Observed: %@"; +"Open an existing Netdata dashboard" = "Open an existing Netdata dashboard"; +"Open dashboard in browser" = "Open dashboard in browser"; +"Opening a dashboard does not import telemetry or verify this host." = "Opening a dashboard does not import telemetry or verify this host."; +"Operation outcome is unresolved. Look up the existing operation before another change." = "Operation outcome is unresolved. Look up the existing operation before another change."; +"Partial" = "Partial"; +"Partial view: limited to 100 recorded sessions." = "Partial view: limited to 100 recorded sessions."; +"Plan digest: %@" = "Plan digest: %@"; +"Plan fallback" = "Plan fallback"; +"Plan switch" = "Plan switch"; +"Primary window: %.1f%% used" = "Primary window: %.1f%% used"; +"Profiles are environment-local. Only provider and stable ID establish cross-environment identity." = "Profiles are environment-local. Only provider and stable ID establish cross-environment identity."; +"Provider usage and API cost" = "Provider usage and API cost"; +"Provider: %@ · Health: %@" = "Provider: %@ · Health: %@"; +"Read-only until CAAM conformance is independently qualified." = "Read-only until CAAM conformance is independently qualified."; +"Recorded cost: %@ · %d days · %@" = "Recorded cost: %@ · %d days · %@"; +"Recorded local sessions only; not a live job queue or evidence of running processes." = "Recorded local sessions only; not a live job queue or evidence of running processes."; +"Refresh and resolve pending operations before changing this environment." = "Refresh and resolve pending operations before changing this environment."; +"Resets: %@" = "Resets: %@"; +"Resolve retained operations before adding more environment observations." = "Resolve retained operations before adding more environment observations."; +"Retained unresolved operation" = "Retained unresolved operation"; +"Review fallback restoration" = "Review fallback restoration"; +"Review interrupted-operation recovery" = "Review interrupted-operation recovery"; +"Review recovery" = "Review recovery"; +"Review switch plan" = "Review switch plan"; +"Runtime effective profile: %@" = "Runtime effective profile: %@"; +"Runtime state: %@" = "Runtime state: %@"; +"Separate metered amount: %@ (not added)" = "Separate metered amount: %@ (not added)"; +"Service and local state" = "Service and local state"; +"Stale" = "Stale"; +"Status and recovery remain bound to the original environment configuration." = "Status and recovery remain bound to the original environment configuration."; +"Systems" = "Systems"; +"The CAAM configuration is invalid." = "The CAAM configuration is invalid."; +"Tokens: %d" = "Tokens: %d"; +"Unknown provenance" = "Unknown provenance"; +"Unresolved operation: %@" = "Unresolved operation: %@"; +"Usage unavailable" = "Usage unavailable"; +"Use HTTPS, or loopback HTTP, without credentials, query parameters, or fragments." = "Use HTTPS, or loopback HTTP, without credentials, query parameters, or fragments."; +"Vendor-metered" = "Vendor-metered"; +"Waiting for CAAM; the app remains available." = "Waiting for CAAM; the app remains available."; +"Window length: %d minutes" = "Window length: %d minutes"; +"Work" = "Work"; diff --git a/Sources/CodexBar/Resources/it.lproj/Localizable.strings b/Sources/CodexBar/Resources/it.lproj/Localizable.strings index f348b58c38..950baca773 100644 --- a/Sources/CodexBar/Resources/it.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/it.lproj/Localizable.strings @@ -1565,3 +1565,78 @@ "SSH" = "SSH"; "SSH destination" = "Destinazione SSH"; "Update required" = "Aggiornamento necessario"; + +/* CAAM observability: English fallback pending translation review. */ +"%@ · Recorded session %d" = "%@ · Recorded session %d"; +"Affected profiles" = "Affected profiles"; +"An unresolved operation belongs to a changed environment. Restore its original configuration." = "An unresolved operation belongs to a changed environment. Restore its original configuration."; +"Available" = "Available"; +"CAAM may complete or roll back its pending transaction. This does not restore fallback." = "CAAM may complete or roll back its pending transaction. This does not restore fallback."; +"CAAM operation tracking is unavailable. Mutations remain disabled." = "CAAM operation tracking is unavailable. Mutations remain disabled."; +"CAAM requires manual intervention on this environment. Automatic recovery is disabled." = "CAAM requires manual intervention on this environment. Automatic recovery is disabled."; +"Change host default: %@ → %@" = "Change host default: %@ → %@"; +"Confirm once" = "Confirm once"; +"Cost unavailable; not zero." = "Cost unavailable; not zero."; +"Dashboard URL (not stored)" = "Dashboard URL (not stored)"; +"Data confidence: %@" = "Data confidence: %@"; +"Estimated API cost: %@" = "Estimated API cost: %@"; +"Expected revision: %@" = "Expected revision: %@"; +"Gateway code: %@" = "Gateway code: %@"; +"Historical provider cache; not a billing receipt or an identified CAAM account." = "Historical provider cache; not a billing receipt or an identified CAAM account."; +"Host default" = "Host default"; +"Host default: %@" = "Host default: %@"; +"Host resources" = "Host resources"; +"Last operation: %@ · %@" = "Last operation: %@ · %@"; +"List-price estimate" = "List-price estimate"; +"Local service state, provider usage and cost, and host resources are separate ledgers." = "Local service state, provider usage and cost, and host resources are separate ledgers."; +"Look up operation" = "Look up operation"; +"Mixed provenance" = "Mixed provenance"; +"Netdata telemetry is unsupported here. No host metrics were collected." = "Netdata telemetry is unsupported here. No host metrics were collected."; +"No account profiles have been observed." = "No account profiles have been observed."; +"No observation timestamp" = "No observation timestamp"; +"No provider observations are available." = "No provider observations are available."; +"No session details are loaded. Use the existing provider refresh to collect supported data." = "No session details are loaded. Use the existing provider refresh to collect supported data."; +"Observability hub" = "Observability hub"; +"Observability view" = "Observability view"; +"Observed: %@" = "Observed: %@"; +"Open an existing Netdata dashboard" = "Open an existing Netdata dashboard"; +"Open dashboard in browser" = "Open dashboard in browser"; +"Opening a dashboard does not import telemetry or verify this host." = "Opening a dashboard does not import telemetry or verify this host."; +"Operation outcome is unresolved. Look up the existing operation before another change." = "Operation outcome is unresolved. Look up the existing operation before another change."; +"Partial" = "Partial"; +"Partial view: limited to 100 recorded sessions." = "Partial view: limited to 100 recorded sessions."; +"Plan digest: %@" = "Plan digest: %@"; +"Plan fallback" = "Plan fallback"; +"Plan switch" = "Plan switch"; +"Primary window: %.1f%% used" = "Primary window: %.1f%% used"; +"Profiles are environment-local. Only provider and stable ID establish cross-environment identity." = "Profiles are environment-local. Only provider and stable ID establish cross-environment identity."; +"Provider usage and API cost" = "Provider usage and API cost"; +"Provider: %@ · Health: %@" = "Provider: %@ · Health: %@"; +"Read-only until CAAM conformance is independently qualified." = "Read-only until CAAM conformance is independently qualified."; +"Recorded cost: %@ · %d days · %@" = "Recorded cost: %@ · %d days · %@"; +"Recorded local sessions only; not a live job queue or evidence of running processes." = "Recorded local sessions only; not a live job queue or evidence of running processes."; +"Refresh and resolve pending operations before changing this environment." = "Refresh and resolve pending operations before changing this environment."; +"Resets: %@" = "Resets: %@"; +"Resolve retained operations before adding more environment observations." = "Resolve retained operations before adding more environment observations."; +"Retained unresolved operation" = "Retained unresolved operation"; +"Review fallback restoration" = "Review fallback restoration"; +"Review interrupted-operation recovery" = "Review interrupted-operation recovery"; +"Review recovery" = "Review recovery"; +"Review switch plan" = "Review switch plan"; +"Runtime effective profile: %@" = "Runtime effective profile: %@"; +"Runtime state: %@" = "Runtime state: %@"; +"Separate metered amount: %@ (not added)" = "Separate metered amount: %@ (not added)"; +"Service and local state" = "Service and local state"; +"Stale" = "Stale"; +"Status and recovery remain bound to the original environment configuration." = "Status and recovery remain bound to the original environment configuration."; +"Systems" = "Systems"; +"The CAAM configuration is invalid." = "The CAAM configuration is invalid."; +"Tokens: %d" = "Tokens: %d"; +"Unknown provenance" = "Unknown provenance"; +"Unresolved operation: %@" = "Unresolved operation: %@"; +"Usage unavailable" = "Usage unavailable"; +"Use HTTPS, or loopback HTTP, without credentials, query parameters, or fragments." = "Use HTTPS, or loopback HTTP, without credentials, query parameters, or fragments."; +"Vendor-metered" = "Vendor-metered"; +"Waiting for CAAM; the app remains available." = "Waiting for CAAM; the app remains available."; +"Window length: %d minutes" = "Window length: %d minutes"; +"Work" = "Work"; diff --git a/Sources/CodexBar/Resources/ja.lproj/Localizable.strings b/Sources/CodexBar/Resources/ja.lproj/Localizable.strings index 57643b9e81..7856f7cb32 100644 --- a/Sources/CodexBar/Resources/ja.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/ja.lproj/Localizable.strings @@ -1562,3 +1562,78 @@ "SSH" = "SSH"; "SSH destination" = "SSH 接続先"; "Update required" = "更新が必要"; + +/* CAAM observability: English fallback pending translation review. */ +"%@ · Recorded session %d" = "%@ · Recorded session %d"; +"Affected profiles" = "Affected profiles"; +"An unresolved operation belongs to a changed environment. Restore its original configuration." = "An unresolved operation belongs to a changed environment. Restore its original configuration."; +"Available" = "Available"; +"CAAM may complete or roll back its pending transaction. This does not restore fallback." = "CAAM may complete or roll back its pending transaction. This does not restore fallback."; +"CAAM operation tracking is unavailable. Mutations remain disabled." = "CAAM operation tracking is unavailable. Mutations remain disabled."; +"CAAM requires manual intervention on this environment. Automatic recovery is disabled." = "CAAM requires manual intervention on this environment. Automatic recovery is disabled."; +"Change host default: %@ → %@" = "Change host default: %@ → %@"; +"Confirm once" = "Confirm once"; +"Cost unavailable; not zero." = "Cost unavailable; not zero."; +"Dashboard URL (not stored)" = "Dashboard URL (not stored)"; +"Data confidence: %@" = "Data confidence: %@"; +"Estimated API cost: %@" = "Estimated API cost: %@"; +"Expected revision: %@" = "Expected revision: %@"; +"Gateway code: %@" = "Gateway code: %@"; +"Historical provider cache; not a billing receipt or an identified CAAM account." = "Historical provider cache; not a billing receipt or an identified CAAM account."; +"Host default" = "Host default"; +"Host default: %@" = "Host default: %@"; +"Host resources" = "Host resources"; +"Last operation: %@ · %@" = "Last operation: %@ · %@"; +"List-price estimate" = "List-price estimate"; +"Local service state, provider usage and cost, and host resources are separate ledgers." = "Local service state, provider usage and cost, and host resources are separate ledgers."; +"Look up operation" = "Look up operation"; +"Mixed provenance" = "Mixed provenance"; +"Netdata telemetry is unsupported here. No host metrics were collected." = "Netdata telemetry is unsupported here. No host metrics were collected."; +"No account profiles have been observed." = "No account profiles have been observed."; +"No observation timestamp" = "No observation timestamp"; +"No provider observations are available." = "No provider observations are available."; +"No session details are loaded. Use the existing provider refresh to collect supported data." = "No session details are loaded. Use the existing provider refresh to collect supported data."; +"Observability hub" = "Observability hub"; +"Observability view" = "Observability view"; +"Observed: %@" = "Observed: %@"; +"Open an existing Netdata dashboard" = "Open an existing Netdata dashboard"; +"Open dashboard in browser" = "Open dashboard in browser"; +"Opening a dashboard does not import telemetry or verify this host." = "Opening a dashboard does not import telemetry or verify this host."; +"Operation outcome is unresolved. Look up the existing operation before another change." = "Operation outcome is unresolved. Look up the existing operation before another change."; +"Partial" = "Partial"; +"Partial view: limited to 100 recorded sessions." = "Partial view: limited to 100 recorded sessions."; +"Plan digest: %@" = "Plan digest: %@"; +"Plan fallback" = "Plan fallback"; +"Plan switch" = "Plan switch"; +"Primary window: %.1f%% used" = "Primary window: %.1f%% used"; +"Profiles are environment-local. Only provider and stable ID establish cross-environment identity." = "Profiles are environment-local. Only provider and stable ID establish cross-environment identity."; +"Provider usage and API cost" = "Provider usage and API cost"; +"Provider: %@ · Health: %@" = "Provider: %@ · Health: %@"; +"Read-only until CAAM conformance is independently qualified." = "Read-only until CAAM conformance is independently qualified."; +"Recorded cost: %@ · %d days · %@" = "Recorded cost: %@ · %d days · %@"; +"Recorded local sessions only; not a live job queue or evidence of running processes." = "Recorded local sessions only; not a live job queue or evidence of running processes."; +"Refresh and resolve pending operations before changing this environment." = "Refresh and resolve pending operations before changing this environment."; +"Resets: %@" = "Resets: %@"; +"Resolve retained operations before adding more environment observations." = "Resolve retained operations before adding more environment observations."; +"Retained unresolved operation" = "Retained unresolved operation"; +"Review fallback restoration" = "Review fallback restoration"; +"Review interrupted-operation recovery" = "Review interrupted-operation recovery"; +"Review recovery" = "Review recovery"; +"Review switch plan" = "Review switch plan"; +"Runtime effective profile: %@" = "Runtime effective profile: %@"; +"Runtime state: %@" = "Runtime state: %@"; +"Separate metered amount: %@ (not added)" = "Separate metered amount: %@ (not added)"; +"Service and local state" = "Service and local state"; +"Stale" = "Stale"; +"Status and recovery remain bound to the original environment configuration." = "Status and recovery remain bound to the original environment configuration."; +"Systems" = "Systems"; +"The CAAM configuration is invalid." = "The CAAM configuration is invalid."; +"Tokens: %d" = "Tokens: %d"; +"Unknown provenance" = "Unknown provenance"; +"Unresolved operation: %@" = "Unresolved operation: %@"; +"Usage unavailable" = "Usage unavailable"; +"Use HTTPS, or loopback HTTP, without credentials, query parameters, or fragments." = "Use HTTPS, or loopback HTTP, without credentials, query parameters, or fragments."; +"Vendor-metered" = "Vendor-metered"; +"Waiting for CAAM; the app remains available." = "Waiting for CAAM; the app remains available."; +"Window length: %d minutes" = "Window length: %d minutes"; +"Work" = "Work"; diff --git a/Sources/CodexBar/Resources/ko.lproj/Localizable.strings b/Sources/CodexBar/Resources/ko.lproj/Localizable.strings index 45bc232abf..fa069065af 100644 --- a/Sources/CodexBar/Resources/ko.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/ko.lproj/Localizable.strings @@ -1531,3 +1531,78 @@ "SSH" = "SSH"; "SSH destination" = "SSH 대상"; "Update required" = "업데이트 필요"; + +/* CAAM observability: English fallback pending translation review. */ +"%@ · Recorded session %d" = "%@ · Recorded session %d"; +"Affected profiles" = "Affected profiles"; +"An unresolved operation belongs to a changed environment. Restore its original configuration." = "An unresolved operation belongs to a changed environment. Restore its original configuration."; +"Available" = "Available"; +"CAAM may complete or roll back its pending transaction. This does not restore fallback." = "CAAM may complete or roll back its pending transaction. This does not restore fallback."; +"CAAM operation tracking is unavailable. Mutations remain disabled." = "CAAM operation tracking is unavailable. Mutations remain disabled."; +"CAAM requires manual intervention on this environment. Automatic recovery is disabled." = "CAAM requires manual intervention on this environment. Automatic recovery is disabled."; +"Change host default: %@ → %@" = "Change host default: %@ → %@"; +"Confirm once" = "Confirm once"; +"Cost unavailable; not zero." = "Cost unavailable; not zero."; +"Dashboard URL (not stored)" = "Dashboard URL (not stored)"; +"Data confidence: %@" = "Data confidence: %@"; +"Estimated API cost: %@" = "Estimated API cost: %@"; +"Expected revision: %@" = "Expected revision: %@"; +"Gateway code: %@" = "Gateway code: %@"; +"Historical provider cache; not a billing receipt or an identified CAAM account." = "Historical provider cache; not a billing receipt or an identified CAAM account."; +"Host default" = "Host default"; +"Host default: %@" = "Host default: %@"; +"Host resources" = "Host resources"; +"Last operation: %@ · %@" = "Last operation: %@ · %@"; +"List-price estimate" = "List-price estimate"; +"Local service state, provider usage and cost, and host resources are separate ledgers." = "Local service state, provider usage and cost, and host resources are separate ledgers."; +"Look up operation" = "Look up operation"; +"Mixed provenance" = "Mixed provenance"; +"Netdata telemetry is unsupported here. No host metrics were collected." = "Netdata telemetry is unsupported here. No host metrics were collected."; +"No account profiles have been observed." = "No account profiles have been observed."; +"No observation timestamp" = "No observation timestamp"; +"No provider observations are available." = "No provider observations are available."; +"No session details are loaded. Use the existing provider refresh to collect supported data." = "No session details are loaded. Use the existing provider refresh to collect supported data."; +"Observability hub" = "Observability hub"; +"Observability view" = "Observability view"; +"Observed: %@" = "Observed: %@"; +"Open an existing Netdata dashboard" = "Open an existing Netdata dashboard"; +"Open dashboard in browser" = "Open dashboard in browser"; +"Opening a dashboard does not import telemetry or verify this host." = "Opening a dashboard does not import telemetry or verify this host."; +"Operation outcome is unresolved. Look up the existing operation before another change." = "Operation outcome is unresolved. Look up the existing operation before another change."; +"Partial" = "Partial"; +"Partial view: limited to 100 recorded sessions." = "Partial view: limited to 100 recorded sessions."; +"Plan digest: %@" = "Plan digest: %@"; +"Plan fallback" = "Plan fallback"; +"Plan switch" = "Plan switch"; +"Primary window: %.1f%% used" = "Primary window: %.1f%% used"; +"Profiles are environment-local. Only provider and stable ID establish cross-environment identity." = "Profiles are environment-local. Only provider and stable ID establish cross-environment identity."; +"Provider usage and API cost" = "Provider usage and API cost"; +"Provider: %@ · Health: %@" = "Provider: %@ · Health: %@"; +"Read-only until CAAM conformance is independently qualified." = "Read-only until CAAM conformance is independently qualified."; +"Recorded cost: %@ · %d days · %@" = "Recorded cost: %@ · %d days · %@"; +"Recorded local sessions only; not a live job queue or evidence of running processes." = "Recorded local sessions only; not a live job queue or evidence of running processes."; +"Refresh and resolve pending operations before changing this environment." = "Refresh and resolve pending operations before changing this environment."; +"Resets: %@" = "Resets: %@"; +"Resolve retained operations before adding more environment observations." = "Resolve retained operations before adding more environment observations."; +"Retained unresolved operation" = "Retained unresolved operation"; +"Review fallback restoration" = "Review fallback restoration"; +"Review interrupted-operation recovery" = "Review interrupted-operation recovery"; +"Review recovery" = "Review recovery"; +"Review switch plan" = "Review switch plan"; +"Runtime effective profile: %@" = "Runtime effective profile: %@"; +"Runtime state: %@" = "Runtime state: %@"; +"Separate metered amount: %@ (not added)" = "Separate metered amount: %@ (not added)"; +"Service and local state" = "Service and local state"; +"Stale" = "Stale"; +"Status and recovery remain bound to the original environment configuration." = "Status and recovery remain bound to the original environment configuration."; +"Systems" = "Systems"; +"The CAAM configuration is invalid." = "The CAAM configuration is invalid."; +"Tokens: %d" = "Tokens: %d"; +"Unknown provenance" = "Unknown provenance"; +"Unresolved operation: %@" = "Unresolved operation: %@"; +"Usage unavailable" = "Usage unavailable"; +"Use HTTPS, or loopback HTTP, without credentials, query parameters, or fragments." = "Use HTTPS, or loopback HTTP, without credentials, query parameters, or fragments."; +"Vendor-metered" = "Vendor-metered"; +"Waiting for CAAM; the app remains available." = "Waiting for CAAM; the app remains available."; +"Window length: %d minutes" = "Window length: %d minutes"; +"Work" = "Work"; diff --git a/Sources/CodexBar/Resources/nl.lproj/Localizable.strings b/Sources/CodexBar/Resources/nl.lproj/Localizable.strings index b4b3139803..5222bd410a 100644 --- a/Sources/CodexBar/Resources/nl.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/nl.lproj/Localizable.strings @@ -1561,3 +1561,78 @@ "SSH" = "SSH"; "SSH destination" = "SSH-bestemming"; "Update required" = "Update vereist"; + +/* CAAM observability: English fallback pending translation review. */ +"%@ · Recorded session %d" = "%@ · Recorded session %d"; +"Affected profiles" = "Affected profiles"; +"An unresolved operation belongs to a changed environment. Restore its original configuration." = "An unresolved operation belongs to a changed environment. Restore its original configuration."; +"Available" = "Available"; +"CAAM may complete or roll back its pending transaction. This does not restore fallback." = "CAAM may complete or roll back its pending transaction. This does not restore fallback."; +"CAAM operation tracking is unavailable. Mutations remain disabled." = "CAAM operation tracking is unavailable. Mutations remain disabled."; +"CAAM requires manual intervention on this environment. Automatic recovery is disabled." = "CAAM requires manual intervention on this environment. Automatic recovery is disabled."; +"Change host default: %@ → %@" = "Change host default: %@ → %@"; +"Confirm once" = "Confirm once"; +"Cost unavailable; not zero." = "Cost unavailable; not zero."; +"Dashboard URL (not stored)" = "Dashboard URL (not stored)"; +"Data confidence: %@" = "Data confidence: %@"; +"Estimated API cost: %@" = "Estimated API cost: %@"; +"Expected revision: %@" = "Expected revision: %@"; +"Gateway code: %@" = "Gateway code: %@"; +"Historical provider cache; not a billing receipt or an identified CAAM account." = "Historical provider cache; not a billing receipt or an identified CAAM account."; +"Host default" = "Host default"; +"Host default: %@" = "Host default: %@"; +"Host resources" = "Host resources"; +"Last operation: %@ · %@" = "Last operation: %@ · %@"; +"List-price estimate" = "List-price estimate"; +"Local service state, provider usage and cost, and host resources are separate ledgers." = "Local service state, provider usage and cost, and host resources are separate ledgers."; +"Look up operation" = "Look up operation"; +"Mixed provenance" = "Mixed provenance"; +"Netdata telemetry is unsupported here. No host metrics were collected." = "Netdata telemetry is unsupported here. No host metrics were collected."; +"No account profiles have been observed." = "No account profiles have been observed."; +"No observation timestamp" = "No observation timestamp"; +"No provider observations are available." = "No provider observations are available."; +"No session details are loaded. Use the existing provider refresh to collect supported data." = "No session details are loaded. Use the existing provider refresh to collect supported data."; +"Observability hub" = "Observability hub"; +"Observability view" = "Observability view"; +"Observed: %@" = "Observed: %@"; +"Open an existing Netdata dashboard" = "Open an existing Netdata dashboard"; +"Open dashboard in browser" = "Open dashboard in browser"; +"Opening a dashboard does not import telemetry or verify this host." = "Opening a dashboard does not import telemetry or verify this host."; +"Operation outcome is unresolved. Look up the existing operation before another change." = "Operation outcome is unresolved. Look up the existing operation before another change."; +"Partial" = "Partial"; +"Partial view: limited to 100 recorded sessions." = "Partial view: limited to 100 recorded sessions."; +"Plan digest: %@" = "Plan digest: %@"; +"Plan fallback" = "Plan fallback"; +"Plan switch" = "Plan switch"; +"Primary window: %.1f%% used" = "Primary window: %.1f%% used"; +"Profiles are environment-local. Only provider and stable ID establish cross-environment identity." = "Profiles are environment-local. Only provider and stable ID establish cross-environment identity."; +"Provider usage and API cost" = "Provider usage and API cost"; +"Provider: %@ · Health: %@" = "Provider: %@ · Health: %@"; +"Read-only until CAAM conformance is independently qualified." = "Read-only until CAAM conformance is independently qualified."; +"Recorded cost: %@ · %d days · %@" = "Recorded cost: %@ · %d days · %@"; +"Recorded local sessions only; not a live job queue or evidence of running processes." = "Recorded local sessions only; not a live job queue or evidence of running processes."; +"Refresh and resolve pending operations before changing this environment." = "Refresh and resolve pending operations before changing this environment."; +"Resets: %@" = "Resets: %@"; +"Resolve retained operations before adding more environment observations." = "Resolve retained operations before adding more environment observations."; +"Retained unresolved operation" = "Retained unresolved operation"; +"Review fallback restoration" = "Review fallback restoration"; +"Review interrupted-operation recovery" = "Review interrupted-operation recovery"; +"Review recovery" = "Review recovery"; +"Review switch plan" = "Review switch plan"; +"Runtime effective profile: %@" = "Runtime effective profile: %@"; +"Runtime state: %@" = "Runtime state: %@"; +"Separate metered amount: %@ (not added)" = "Separate metered amount: %@ (not added)"; +"Service and local state" = "Service and local state"; +"Stale" = "Stale"; +"Status and recovery remain bound to the original environment configuration." = "Status and recovery remain bound to the original environment configuration."; +"Systems" = "Systems"; +"The CAAM configuration is invalid." = "The CAAM configuration is invalid."; +"Tokens: %d" = "Tokens: %d"; +"Unknown provenance" = "Unknown provenance"; +"Unresolved operation: %@" = "Unresolved operation: %@"; +"Usage unavailable" = "Usage unavailable"; +"Use HTTPS, or loopback HTTP, without credentials, query parameters, or fragments." = "Use HTTPS, or loopback HTTP, without credentials, query parameters, or fragments."; +"Vendor-metered" = "Vendor-metered"; +"Waiting for CAAM; the app remains available." = "Waiting for CAAM; the app remains available."; +"Window length: %d minutes" = "Window length: %d minutes"; +"Work" = "Work"; diff --git a/Sources/CodexBar/Resources/pl.lproj/Localizable.strings b/Sources/CodexBar/Resources/pl.lproj/Localizable.strings index 55706a56f9..5f55763a90 100644 --- a/Sources/CodexBar/Resources/pl.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/pl.lproj/Localizable.strings @@ -1565,3 +1565,78 @@ "SSH" = "SSH"; "SSH destination" = "Miejsce docelowe SSH"; "Update required" = "Wymagana aktualizacja"; + +/* CAAM observability: English fallback pending translation review. */ +"%@ · Recorded session %d" = "%@ · Recorded session %d"; +"Affected profiles" = "Affected profiles"; +"An unresolved operation belongs to a changed environment. Restore its original configuration." = "An unresolved operation belongs to a changed environment. Restore its original configuration."; +"Available" = "Available"; +"CAAM may complete or roll back its pending transaction. This does not restore fallback." = "CAAM may complete or roll back its pending transaction. This does not restore fallback."; +"CAAM operation tracking is unavailable. Mutations remain disabled." = "CAAM operation tracking is unavailable. Mutations remain disabled."; +"CAAM requires manual intervention on this environment. Automatic recovery is disabled." = "CAAM requires manual intervention on this environment. Automatic recovery is disabled."; +"Change host default: %@ → %@" = "Change host default: %@ → %@"; +"Confirm once" = "Confirm once"; +"Cost unavailable; not zero." = "Cost unavailable; not zero."; +"Dashboard URL (not stored)" = "Dashboard URL (not stored)"; +"Data confidence: %@" = "Data confidence: %@"; +"Estimated API cost: %@" = "Estimated API cost: %@"; +"Expected revision: %@" = "Expected revision: %@"; +"Gateway code: %@" = "Gateway code: %@"; +"Historical provider cache; not a billing receipt or an identified CAAM account." = "Historical provider cache; not a billing receipt or an identified CAAM account."; +"Host default" = "Host default"; +"Host default: %@" = "Host default: %@"; +"Host resources" = "Host resources"; +"Last operation: %@ · %@" = "Last operation: %@ · %@"; +"List-price estimate" = "List-price estimate"; +"Local service state, provider usage and cost, and host resources are separate ledgers." = "Local service state, provider usage and cost, and host resources are separate ledgers."; +"Look up operation" = "Look up operation"; +"Mixed provenance" = "Mixed provenance"; +"Netdata telemetry is unsupported here. No host metrics were collected." = "Netdata telemetry is unsupported here. No host metrics were collected."; +"No account profiles have been observed." = "No account profiles have been observed."; +"No observation timestamp" = "No observation timestamp"; +"No provider observations are available." = "No provider observations are available."; +"No session details are loaded. Use the existing provider refresh to collect supported data." = "No session details are loaded. Use the existing provider refresh to collect supported data."; +"Observability hub" = "Observability hub"; +"Observability view" = "Observability view"; +"Observed: %@" = "Observed: %@"; +"Open an existing Netdata dashboard" = "Open an existing Netdata dashboard"; +"Open dashboard in browser" = "Open dashboard in browser"; +"Opening a dashboard does not import telemetry or verify this host." = "Opening a dashboard does not import telemetry or verify this host."; +"Operation outcome is unresolved. Look up the existing operation before another change." = "Operation outcome is unresolved. Look up the existing operation before another change."; +"Partial" = "Partial"; +"Partial view: limited to 100 recorded sessions." = "Partial view: limited to 100 recorded sessions."; +"Plan digest: %@" = "Plan digest: %@"; +"Plan fallback" = "Plan fallback"; +"Plan switch" = "Plan switch"; +"Primary window: %.1f%% used" = "Primary window: %.1f%% used"; +"Profiles are environment-local. Only provider and stable ID establish cross-environment identity." = "Profiles are environment-local. Only provider and stable ID establish cross-environment identity."; +"Provider usage and API cost" = "Provider usage and API cost"; +"Provider: %@ · Health: %@" = "Provider: %@ · Health: %@"; +"Read-only until CAAM conformance is independently qualified." = "Read-only until CAAM conformance is independently qualified."; +"Recorded cost: %@ · %d days · %@" = "Recorded cost: %@ · %d days · %@"; +"Recorded local sessions only; not a live job queue or evidence of running processes." = "Recorded local sessions only; not a live job queue or evidence of running processes."; +"Refresh and resolve pending operations before changing this environment." = "Refresh and resolve pending operations before changing this environment."; +"Resets: %@" = "Resets: %@"; +"Resolve retained operations before adding more environment observations." = "Resolve retained operations before adding more environment observations."; +"Retained unresolved operation" = "Retained unresolved operation"; +"Review fallback restoration" = "Review fallback restoration"; +"Review interrupted-operation recovery" = "Review interrupted-operation recovery"; +"Review recovery" = "Review recovery"; +"Review switch plan" = "Review switch plan"; +"Runtime effective profile: %@" = "Runtime effective profile: %@"; +"Runtime state: %@" = "Runtime state: %@"; +"Separate metered amount: %@ (not added)" = "Separate metered amount: %@ (not added)"; +"Service and local state" = "Service and local state"; +"Stale" = "Stale"; +"Status and recovery remain bound to the original environment configuration." = "Status and recovery remain bound to the original environment configuration."; +"Systems" = "Systems"; +"The CAAM configuration is invalid." = "The CAAM configuration is invalid."; +"Tokens: %d" = "Tokens: %d"; +"Unknown provenance" = "Unknown provenance"; +"Unresolved operation: %@" = "Unresolved operation: %@"; +"Usage unavailable" = "Usage unavailable"; +"Use HTTPS, or loopback HTTP, without credentials, query parameters, or fragments." = "Use HTTPS, or loopback HTTP, without credentials, query parameters, or fragments."; +"Vendor-metered" = "Vendor-metered"; +"Waiting for CAAM; the app remains available." = "Waiting for CAAM; the app remains available."; +"Window length: %d minutes" = "Window length: %d minutes"; +"Work" = "Work"; diff --git a/Sources/CodexBar/Resources/pt-BR.lproj/Localizable.strings b/Sources/CodexBar/Resources/pt-BR.lproj/Localizable.strings index 501ed19f82..ebbbd56258 100644 --- a/Sources/CodexBar/Resources/pt-BR.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/pt-BR.lproj/Localizable.strings @@ -1562,3 +1562,78 @@ "SSH" = "SSH"; "SSH destination" = "Destino SSH"; "Update required" = "Atualização necessária"; + +/* CAAM observability: English fallback pending translation review. */ +"%@ · Recorded session %d" = "%@ · Recorded session %d"; +"Affected profiles" = "Affected profiles"; +"An unresolved operation belongs to a changed environment. Restore its original configuration." = "An unresolved operation belongs to a changed environment. Restore its original configuration."; +"Available" = "Available"; +"CAAM may complete or roll back its pending transaction. This does not restore fallback." = "CAAM may complete or roll back its pending transaction. This does not restore fallback."; +"CAAM operation tracking is unavailable. Mutations remain disabled." = "CAAM operation tracking is unavailable. Mutations remain disabled."; +"CAAM requires manual intervention on this environment. Automatic recovery is disabled." = "CAAM requires manual intervention on this environment. Automatic recovery is disabled."; +"Change host default: %@ → %@" = "Change host default: %@ → %@"; +"Confirm once" = "Confirm once"; +"Cost unavailable; not zero." = "Cost unavailable; not zero."; +"Dashboard URL (not stored)" = "Dashboard URL (not stored)"; +"Data confidence: %@" = "Data confidence: %@"; +"Estimated API cost: %@" = "Estimated API cost: %@"; +"Expected revision: %@" = "Expected revision: %@"; +"Gateway code: %@" = "Gateway code: %@"; +"Historical provider cache; not a billing receipt or an identified CAAM account." = "Historical provider cache; not a billing receipt or an identified CAAM account."; +"Host default" = "Host default"; +"Host default: %@" = "Host default: %@"; +"Host resources" = "Host resources"; +"Last operation: %@ · %@" = "Last operation: %@ · %@"; +"List-price estimate" = "List-price estimate"; +"Local service state, provider usage and cost, and host resources are separate ledgers." = "Local service state, provider usage and cost, and host resources are separate ledgers."; +"Look up operation" = "Look up operation"; +"Mixed provenance" = "Mixed provenance"; +"Netdata telemetry is unsupported here. No host metrics were collected." = "Netdata telemetry is unsupported here. No host metrics were collected."; +"No account profiles have been observed." = "No account profiles have been observed."; +"No observation timestamp" = "No observation timestamp"; +"No provider observations are available." = "No provider observations are available."; +"No session details are loaded. Use the existing provider refresh to collect supported data." = "No session details are loaded. Use the existing provider refresh to collect supported data."; +"Observability hub" = "Observability hub"; +"Observability view" = "Observability view"; +"Observed: %@" = "Observed: %@"; +"Open an existing Netdata dashboard" = "Open an existing Netdata dashboard"; +"Open dashboard in browser" = "Open dashboard in browser"; +"Opening a dashboard does not import telemetry or verify this host." = "Opening a dashboard does not import telemetry or verify this host."; +"Operation outcome is unresolved. Look up the existing operation before another change." = "Operation outcome is unresolved. Look up the existing operation before another change."; +"Partial" = "Partial"; +"Partial view: limited to 100 recorded sessions." = "Partial view: limited to 100 recorded sessions."; +"Plan digest: %@" = "Plan digest: %@"; +"Plan fallback" = "Plan fallback"; +"Plan switch" = "Plan switch"; +"Primary window: %.1f%% used" = "Primary window: %.1f%% used"; +"Profiles are environment-local. Only provider and stable ID establish cross-environment identity." = "Profiles are environment-local. Only provider and stable ID establish cross-environment identity."; +"Provider usage and API cost" = "Provider usage and API cost"; +"Provider: %@ · Health: %@" = "Provider: %@ · Health: %@"; +"Read-only until CAAM conformance is independently qualified." = "Read-only until CAAM conformance is independently qualified."; +"Recorded cost: %@ · %d days · %@" = "Recorded cost: %@ · %d days · %@"; +"Recorded local sessions only; not a live job queue or evidence of running processes." = "Recorded local sessions only; not a live job queue or evidence of running processes."; +"Refresh and resolve pending operations before changing this environment." = "Refresh and resolve pending operations before changing this environment."; +"Resets: %@" = "Resets: %@"; +"Resolve retained operations before adding more environment observations." = "Resolve retained operations before adding more environment observations."; +"Retained unresolved operation" = "Retained unresolved operation"; +"Review fallback restoration" = "Review fallback restoration"; +"Review interrupted-operation recovery" = "Review interrupted-operation recovery"; +"Review recovery" = "Review recovery"; +"Review switch plan" = "Review switch plan"; +"Runtime effective profile: %@" = "Runtime effective profile: %@"; +"Runtime state: %@" = "Runtime state: %@"; +"Separate metered amount: %@ (not added)" = "Separate metered amount: %@ (not added)"; +"Service and local state" = "Service and local state"; +"Stale" = "Stale"; +"Status and recovery remain bound to the original environment configuration." = "Status and recovery remain bound to the original environment configuration."; +"Systems" = "Systems"; +"The CAAM configuration is invalid." = "The CAAM configuration is invalid."; +"Tokens: %d" = "Tokens: %d"; +"Unknown provenance" = "Unknown provenance"; +"Unresolved operation: %@" = "Unresolved operation: %@"; +"Usage unavailable" = "Usage unavailable"; +"Use HTTPS, or loopback HTTP, without credentials, query parameters, or fragments." = "Use HTTPS, or loopback HTTP, without credentials, query parameters, or fragments."; +"Vendor-metered" = "Vendor-metered"; +"Waiting for CAAM; the app remains available." = "Waiting for CAAM; the app remains available."; +"Window length: %d minutes" = "Window length: %d minutes"; +"Work" = "Work"; diff --git a/Sources/CodexBar/Resources/ru.lproj/Localizable.strings b/Sources/CodexBar/Resources/ru.lproj/Localizable.strings index 446218b1ef..9ae5096f55 100644 --- a/Sources/CodexBar/Resources/ru.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/ru.lproj/Localizable.strings @@ -1563,3 +1563,78 @@ "SSH" = "SSH"; "SSH destination" = "Адрес SSH"; "Update required" = "Требуется обновление"; + +/* CAAM observability: English fallback pending translation review. */ +"%@ · Recorded session %d" = "%@ · Recorded session %d"; +"Affected profiles" = "Affected profiles"; +"An unresolved operation belongs to a changed environment. Restore its original configuration." = "An unresolved operation belongs to a changed environment. Restore its original configuration."; +"Available" = "Available"; +"CAAM may complete or roll back its pending transaction. This does not restore fallback." = "CAAM may complete or roll back its pending transaction. This does not restore fallback."; +"CAAM operation tracking is unavailable. Mutations remain disabled." = "CAAM operation tracking is unavailable. Mutations remain disabled."; +"CAAM requires manual intervention on this environment. Automatic recovery is disabled." = "CAAM requires manual intervention on this environment. Automatic recovery is disabled."; +"Change host default: %@ → %@" = "Change host default: %@ → %@"; +"Confirm once" = "Confirm once"; +"Cost unavailable; not zero." = "Cost unavailable; not zero."; +"Dashboard URL (not stored)" = "Dashboard URL (not stored)"; +"Data confidence: %@" = "Data confidence: %@"; +"Estimated API cost: %@" = "Estimated API cost: %@"; +"Expected revision: %@" = "Expected revision: %@"; +"Gateway code: %@" = "Gateway code: %@"; +"Historical provider cache; not a billing receipt or an identified CAAM account." = "Historical provider cache; not a billing receipt or an identified CAAM account."; +"Host default" = "Host default"; +"Host default: %@" = "Host default: %@"; +"Host resources" = "Host resources"; +"Last operation: %@ · %@" = "Last operation: %@ · %@"; +"List-price estimate" = "List-price estimate"; +"Local service state, provider usage and cost, and host resources are separate ledgers." = "Local service state, provider usage and cost, and host resources are separate ledgers."; +"Look up operation" = "Look up operation"; +"Mixed provenance" = "Mixed provenance"; +"Netdata telemetry is unsupported here. No host metrics were collected." = "Netdata telemetry is unsupported here. No host metrics were collected."; +"No account profiles have been observed." = "No account profiles have been observed."; +"No observation timestamp" = "No observation timestamp"; +"No provider observations are available." = "No provider observations are available."; +"No session details are loaded. Use the existing provider refresh to collect supported data." = "No session details are loaded. Use the existing provider refresh to collect supported data."; +"Observability hub" = "Observability hub"; +"Observability view" = "Observability view"; +"Observed: %@" = "Observed: %@"; +"Open an existing Netdata dashboard" = "Open an existing Netdata dashboard"; +"Open dashboard in browser" = "Open dashboard in browser"; +"Opening a dashboard does not import telemetry or verify this host." = "Opening a dashboard does not import telemetry or verify this host."; +"Operation outcome is unresolved. Look up the existing operation before another change." = "Operation outcome is unresolved. Look up the existing operation before another change."; +"Partial" = "Partial"; +"Partial view: limited to 100 recorded sessions." = "Partial view: limited to 100 recorded sessions."; +"Plan digest: %@" = "Plan digest: %@"; +"Plan fallback" = "Plan fallback"; +"Plan switch" = "Plan switch"; +"Primary window: %.1f%% used" = "Primary window: %.1f%% used"; +"Profiles are environment-local. Only provider and stable ID establish cross-environment identity." = "Profiles are environment-local. Only provider and stable ID establish cross-environment identity."; +"Provider usage and API cost" = "Provider usage and API cost"; +"Provider: %@ · Health: %@" = "Provider: %@ · Health: %@"; +"Read-only until CAAM conformance is independently qualified." = "Read-only until CAAM conformance is independently qualified."; +"Recorded cost: %@ · %d days · %@" = "Recorded cost: %@ · %d days · %@"; +"Recorded local sessions only; not a live job queue or evidence of running processes." = "Recorded local sessions only; not a live job queue or evidence of running processes."; +"Refresh and resolve pending operations before changing this environment." = "Refresh and resolve pending operations before changing this environment."; +"Resets: %@" = "Resets: %@"; +"Resolve retained operations before adding more environment observations." = "Resolve retained operations before adding more environment observations."; +"Retained unresolved operation" = "Retained unresolved operation"; +"Review fallback restoration" = "Review fallback restoration"; +"Review interrupted-operation recovery" = "Review interrupted-operation recovery"; +"Review recovery" = "Review recovery"; +"Review switch plan" = "Review switch plan"; +"Runtime effective profile: %@" = "Runtime effective profile: %@"; +"Runtime state: %@" = "Runtime state: %@"; +"Separate metered amount: %@ (not added)" = "Separate metered amount: %@ (not added)"; +"Service and local state" = "Service and local state"; +"Stale" = "Stale"; +"Status and recovery remain bound to the original environment configuration." = "Status and recovery remain bound to the original environment configuration."; +"Systems" = "Systems"; +"The CAAM configuration is invalid." = "The CAAM configuration is invalid."; +"Tokens: %d" = "Tokens: %d"; +"Unknown provenance" = "Unknown provenance"; +"Unresolved operation: %@" = "Unresolved operation: %@"; +"Usage unavailable" = "Usage unavailable"; +"Use HTTPS, or loopback HTTP, without credentials, query parameters, or fragments." = "Use HTTPS, or loopback HTTP, without credentials, query parameters, or fragments."; +"Vendor-metered" = "Vendor-metered"; +"Waiting for CAAM; the app remains available." = "Waiting for CAAM; the app remains available."; +"Window length: %d minutes" = "Window length: %d minutes"; +"Work" = "Work"; diff --git a/Sources/CodexBar/Resources/sv.lproj/Localizable.strings b/Sources/CodexBar/Resources/sv.lproj/Localizable.strings index 921fdfab72..eb35bd84bb 100644 --- a/Sources/CodexBar/Resources/sv.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/sv.lproj/Localizable.strings @@ -1561,3 +1561,78 @@ "SSH" = "SSH"; "SSH destination" = "SSH-mål"; "Update required" = "Uppdatering krävs"; + +/* CAAM observability: English fallback pending translation review. */ +"%@ · Recorded session %d" = "%@ · Recorded session %d"; +"Affected profiles" = "Affected profiles"; +"An unresolved operation belongs to a changed environment. Restore its original configuration." = "An unresolved operation belongs to a changed environment. Restore its original configuration."; +"Available" = "Available"; +"CAAM may complete or roll back its pending transaction. This does not restore fallback." = "CAAM may complete or roll back its pending transaction. This does not restore fallback."; +"CAAM operation tracking is unavailable. Mutations remain disabled." = "CAAM operation tracking is unavailable. Mutations remain disabled."; +"CAAM requires manual intervention on this environment. Automatic recovery is disabled." = "CAAM requires manual intervention on this environment. Automatic recovery is disabled."; +"Change host default: %@ → %@" = "Change host default: %@ → %@"; +"Confirm once" = "Confirm once"; +"Cost unavailable; not zero." = "Cost unavailable; not zero."; +"Dashboard URL (not stored)" = "Dashboard URL (not stored)"; +"Data confidence: %@" = "Data confidence: %@"; +"Estimated API cost: %@" = "Estimated API cost: %@"; +"Expected revision: %@" = "Expected revision: %@"; +"Gateway code: %@" = "Gateway code: %@"; +"Historical provider cache; not a billing receipt or an identified CAAM account." = "Historical provider cache; not a billing receipt or an identified CAAM account."; +"Host default" = "Host default"; +"Host default: %@" = "Host default: %@"; +"Host resources" = "Host resources"; +"Last operation: %@ · %@" = "Last operation: %@ · %@"; +"List-price estimate" = "List-price estimate"; +"Local service state, provider usage and cost, and host resources are separate ledgers." = "Local service state, provider usage and cost, and host resources are separate ledgers."; +"Look up operation" = "Look up operation"; +"Mixed provenance" = "Mixed provenance"; +"Netdata telemetry is unsupported here. No host metrics were collected." = "Netdata telemetry is unsupported here. No host metrics were collected."; +"No account profiles have been observed." = "No account profiles have been observed."; +"No observation timestamp" = "No observation timestamp"; +"No provider observations are available." = "No provider observations are available."; +"No session details are loaded. Use the existing provider refresh to collect supported data." = "No session details are loaded. Use the existing provider refresh to collect supported data."; +"Observability hub" = "Observability hub"; +"Observability view" = "Observability view"; +"Observed: %@" = "Observed: %@"; +"Open an existing Netdata dashboard" = "Open an existing Netdata dashboard"; +"Open dashboard in browser" = "Open dashboard in browser"; +"Opening a dashboard does not import telemetry or verify this host." = "Opening a dashboard does not import telemetry or verify this host."; +"Operation outcome is unresolved. Look up the existing operation before another change." = "Operation outcome is unresolved. Look up the existing operation before another change."; +"Partial" = "Partial"; +"Partial view: limited to 100 recorded sessions." = "Partial view: limited to 100 recorded sessions."; +"Plan digest: %@" = "Plan digest: %@"; +"Plan fallback" = "Plan fallback"; +"Plan switch" = "Plan switch"; +"Primary window: %.1f%% used" = "Primary window: %.1f%% used"; +"Profiles are environment-local. Only provider and stable ID establish cross-environment identity." = "Profiles are environment-local. Only provider and stable ID establish cross-environment identity."; +"Provider usage and API cost" = "Provider usage and API cost"; +"Provider: %@ · Health: %@" = "Provider: %@ · Health: %@"; +"Read-only until CAAM conformance is independently qualified." = "Read-only until CAAM conformance is independently qualified."; +"Recorded cost: %@ · %d days · %@" = "Recorded cost: %@ · %d days · %@"; +"Recorded local sessions only; not a live job queue or evidence of running processes." = "Recorded local sessions only; not a live job queue or evidence of running processes."; +"Refresh and resolve pending operations before changing this environment." = "Refresh and resolve pending operations before changing this environment."; +"Resets: %@" = "Resets: %@"; +"Resolve retained operations before adding more environment observations." = "Resolve retained operations before adding more environment observations."; +"Retained unresolved operation" = "Retained unresolved operation"; +"Review fallback restoration" = "Review fallback restoration"; +"Review interrupted-operation recovery" = "Review interrupted-operation recovery"; +"Review recovery" = "Review recovery"; +"Review switch plan" = "Review switch plan"; +"Runtime effective profile: %@" = "Runtime effective profile: %@"; +"Runtime state: %@" = "Runtime state: %@"; +"Separate metered amount: %@ (not added)" = "Separate metered amount: %@ (not added)"; +"Service and local state" = "Service and local state"; +"Stale" = "Stale"; +"Status and recovery remain bound to the original environment configuration." = "Status and recovery remain bound to the original environment configuration."; +"Systems" = "Systems"; +"The CAAM configuration is invalid." = "The CAAM configuration is invalid."; +"Tokens: %d" = "Tokens: %d"; +"Unknown provenance" = "Unknown provenance"; +"Unresolved operation: %@" = "Unresolved operation: %@"; +"Usage unavailable" = "Usage unavailable"; +"Use HTTPS, or loopback HTTP, without credentials, query parameters, or fragments." = "Use HTTPS, or loopback HTTP, without credentials, query parameters, or fragments."; +"Vendor-metered" = "Vendor-metered"; +"Waiting for CAAM; the app remains available." = "Waiting for CAAM; the app remains available."; +"Window length: %d minutes" = "Window length: %d minutes"; +"Work" = "Work"; diff --git a/Sources/CodexBar/Resources/th.lproj/Localizable.strings b/Sources/CodexBar/Resources/th.lproj/Localizable.strings index f6b24a327f..1ebd783933 100644 --- a/Sources/CodexBar/Resources/th.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/th.lproj/Localizable.strings @@ -1566,3 +1566,78 @@ "SSH" = "SSH"; "SSH destination" = "ปลายทาง SSH"; "Update required" = "จำเป็นต้องอัปเดต"; + +/* CAAM observability: English fallback pending translation review. */ +"%@ · Recorded session %d" = "%@ · Recorded session %d"; +"Affected profiles" = "Affected profiles"; +"An unresolved operation belongs to a changed environment. Restore its original configuration." = "An unresolved operation belongs to a changed environment. Restore its original configuration."; +"Available" = "Available"; +"CAAM may complete or roll back its pending transaction. This does not restore fallback." = "CAAM may complete or roll back its pending transaction. This does not restore fallback."; +"CAAM operation tracking is unavailable. Mutations remain disabled." = "CAAM operation tracking is unavailable. Mutations remain disabled."; +"CAAM requires manual intervention on this environment. Automatic recovery is disabled." = "CAAM requires manual intervention on this environment. Automatic recovery is disabled."; +"Change host default: %@ → %@" = "Change host default: %@ → %@"; +"Confirm once" = "Confirm once"; +"Cost unavailable; not zero." = "Cost unavailable; not zero."; +"Dashboard URL (not stored)" = "Dashboard URL (not stored)"; +"Data confidence: %@" = "Data confidence: %@"; +"Estimated API cost: %@" = "Estimated API cost: %@"; +"Expected revision: %@" = "Expected revision: %@"; +"Gateway code: %@" = "Gateway code: %@"; +"Historical provider cache; not a billing receipt or an identified CAAM account." = "Historical provider cache; not a billing receipt or an identified CAAM account."; +"Host default" = "Host default"; +"Host default: %@" = "Host default: %@"; +"Host resources" = "Host resources"; +"Last operation: %@ · %@" = "Last operation: %@ · %@"; +"List-price estimate" = "List-price estimate"; +"Local service state, provider usage and cost, and host resources are separate ledgers." = "Local service state, provider usage and cost, and host resources are separate ledgers."; +"Look up operation" = "Look up operation"; +"Mixed provenance" = "Mixed provenance"; +"Netdata telemetry is unsupported here. No host metrics were collected." = "Netdata telemetry is unsupported here. No host metrics were collected."; +"No account profiles have been observed." = "No account profiles have been observed."; +"No observation timestamp" = "No observation timestamp"; +"No provider observations are available." = "No provider observations are available."; +"No session details are loaded. Use the existing provider refresh to collect supported data." = "No session details are loaded. Use the existing provider refresh to collect supported data."; +"Observability hub" = "Observability hub"; +"Observability view" = "Observability view"; +"Observed: %@" = "Observed: %@"; +"Open an existing Netdata dashboard" = "Open an existing Netdata dashboard"; +"Open dashboard in browser" = "Open dashboard in browser"; +"Opening a dashboard does not import telemetry or verify this host." = "Opening a dashboard does not import telemetry or verify this host."; +"Operation outcome is unresolved. Look up the existing operation before another change." = "Operation outcome is unresolved. Look up the existing operation before another change."; +"Partial" = "Partial"; +"Partial view: limited to 100 recorded sessions." = "Partial view: limited to 100 recorded sessions."; +"Plan digest: %@" = "Plan digest: %@"; +"Plan fallback" = "Plan fallback"; +"Plan switch" = "Plan switch"; +"Primary window: %.1f%% used" = "Primary window: %.1f%% used"; +"Profiles are environment-local. Only provider and stable ID establish cross-environment identity." = "Profiles are environment-local. Only provider and stable ID establish cross-environment identity."; +"Provider usage and API cost" = "Provider usage and API cost"; +"Provider: %@ · Health: %@" = "Provider: %@ · Health: %@"; +"Read-only until CAAM conformance is independently qualified." = "Read-only until CAAM conformance is independently qualified."; +"Recorded cost: %@ · %d days · %@" = "Recorded cost: %@ · %d days · %@"; +"Recorded local sessions only; not a live job queue or evidence of running processes." = "Recorded local sessions only; not a live job queue or evidence of running processes."; +"Refresh and resolve pending operations before changing this environment." = "Refresh and resolve pending operations before changing this environment."; +"Resets: %@" = "Resets: %@"; +"Resolve retained operations before adding more environment observations." = "Resolve retained operations before adding more environment observations."; +"Retained unresolved operation" = "Retained unresolved operation"; +"Review fallback restoration" = "Review fallback restoration"; +"Review interrupted-operation recovery" = "Review interrupted-operation recovery"; +"Review recovery" = "Review recovery"; +"Review switch plan" = "Review switch plan"; +"Runtime effective profile: %@" = "Runtime effective profile: %@"; +"Runtime state: %@" = "Runtime state: %@"; +"Separate metered amount: %@ (not added)" = "Separate metered amount: %@ (not added)"; +"Service and local state" = "Service and local state"; +"Stale" = "Stale"; +"Status and recovery remain bound to the original environment configuration." = "Status and recovery remain bound to the original environment configuration."; +"Systems" = "Systems"; +"The CAAM configuration is invalid." = "The CAAM configuration is invalid."; +"Tokens: %d" = "Tokens: %d"; +"Unknown provenance" = "Unknown provenance"; +"Unresolved operation: %@" = "Unresolved operation: %@"; +"Usage unavailable" = "Usage unavailable"; +"Use HTTPS, or loopback HTTP, without credentials, query parameters, or fragments." = "Use HTTPS, or loopback HTTP, without credentials, query parameters, or fragments."; +"Vendor-metered" = "Vendor-metered"; +"Waiting for CAAM; the app remains available." = "Waiting for CAAM; the app remains available."; +"Window length: %d minutes" = "Window length: %d minutes"; +"Work" = "Work"; diff --git a/Sources/CodexBar/Resources/tr.lproj/Localizable.strings b/Sources/CodexBar/Resources/tr.lproj/Localizable.strings index a3852cbfcc..e410079c8d 100644 --- a/Sources/CodexBar/Resources/tr.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/tr.lproj/Localizable.strings @@ -1564,3 +1564,78 @@ "SSH" = "SSH"; "SSH destination" = "SSH hedefi"; "Update required" = "Güncelleme gerekli"; + +/* CAAM observability: English fallback pending translation review. */ +"%@ · Recorded session %d" = "%@ · Recorded session %d"; +"Affected profiles" = "Affected profiles"; +"An unresolved operation belongs to a changed environment. Restore its original configuration." = "An unresolved operation belongs to a changed environment. Restore its original configuration."; +"Available" = "Available"; +"CAAM may complete or roll back its pending transaction. This does not restore fallback." = "CAAM may complete or roll back its pending transaction. This does not restore fallback."; +"CAAM operation tracking is unavailable. Mutations remain disabled." = "CAAM operation tracking is unavailable. Mutations remain disabled."; +"CAAM requires manual intervention on this environment. Automatic recovery is disabled." = "CAAM requires manual intervention on this environment. Automatic recovery is disabled."; +"Change host default: %@ → %@" = "Change host default: %@ → %@"; +"Confirm once" = "Confirm once"; +"Cost unavailable; not zero." = "Cost unavailable; not zero."; +"Dashboard URL (not stored)" = "Dashboard URL (not stored)"; +"Data confidence: %@" = "Data confidence: %@"; +"Estimated API cost: %@" = "Estimated API cost: %@"; +"Expected revision: %@" = "Expected revision: %@"; +"Gateway code: %@" = "Gateway code: %@"; +"Historical provider cache; not a billing receipt or an identified CAAM account." = "Historical provider cache; not a billing receipt or an identified CAAM account."; +"Host default" = "Host default"; +"Host default: %@" = "Host default: %@"; +"Host resources" = "Host resources"; +"Last operation: %@ · %@" = "Last operation: %@ · %@"; +"List-price estimate" = "List-price estimate"; +"Local service state, provider usage and cost, and host resources are separate ledgers." = "Local service state, provider usage and cost, and host resources are separate ledgers."; +"Look up operation" = "Look up operation"; +"Mixed provenance" = "Mixed provenance"; +"Netdata telemetry is unsupported here. No host metrics were collected." = "Netdata telemetry is unsupported here. No host metrics were collected."; +"No account profiles have been observed." = "No account profiles have been observed."; +"No observation timestamp" = "No observation timestamp"; +"No provider observations are available." = "No provider observations are available."; +"No session details are loaded. Use the existing provider refresh to collect supported data." = "No session details are loaded. Use the existing provider refresh to collect supported data."; +"Observability hub" = "Observability hub"; +"Observability view" = "Observability view"; +"Observed: %@" = "Observed: %@"; +"Open an existing Netdata dashboard" = "Open an existing Netdata dashboard"; +"Open dashboard in browser" = "Open dashboard in browser"; +"Opening a dashboard does not import telemetry or verify this host." = "Opening a dashboard does not import telemetry or verify this host."; +"Operation outcome is unresolved. Look up the existing operation before another change." = "Operation outcome is unresolved. Look up the existing operation before another change."; +"Partial" = "Partial"; +"Partial view: limited to 100 recorded sessions." = "Partial view: limited to 100 recorded sessions."; +"Plan digest: %@" = "Plan digest: %@"; +"Plan fallback" = "Plan fallback"; +"Plan switch" = "Plan switch"; +"Primary window: %.1f%% used" = "Primary window: %.1f%% used"; +"Profiles are environment-local. Only provider and stable ID establish cross-environment identity." = "Profiles are environment-local. Only provider and stable ID establish cross-environment identity."; +"Provider usage and API cost" = "Provider usage and API cost"; +"Provider: %@ · Health: %@" = "Provider: %@ · Health: %@"; +"Read-only until CAAM conformance is independently qualified." = "Read-only until CAAM conformance is independently qualified."; +"Recorded cost: %@ · %d days · %@" = "Recorded cost: %@ · %d days · %@"; +"Recorded local sessions only; not a live job queue or evidence of running processes." = "Recorded local sessions only; not a live job queue or evidence of running processes."; +"Refresh and resolve pending operations before changing this environment." = "Refresh and resolve pending operations before changing this environment."; +"Resets: %@" = "Resets: %@"; +"Resolve retained operations before adding more environment observations." = "Resolve retained operations before adding more environment observations."; +"Retained unresolved operation" = "Retained unresolved operation"; +"Review fallback restoration" = "Review fallback restoration"; +"Review interrupted-operation recovery" = "Review interrupted-operation recovery"; +"Review recovery" = "Review recovery"; +"Review switch plan" = "Review switch plan"; +"Runtime effective profile: %@" = "Runtime effective profile: %@"; +"Runtime state: %@" = "Runtime state: %@"; +"Separate metered amount: %@ (not added)" = "Separate metered amount: %@ (not added)"; +"Service and local state" = "Service and local state"; +"Stale" = "Stale"; +"Status and recovery remain bound to the original environment configuration." = "Status and recovery remain bound to the original environment configuration."; +"Systems" = "Systems"; +"The CAAM configuration is invalid." = "The CAAM configuration is invalid."; +"Tokens: %d" = "Tokens: %d"; +"Unknown provenance" = "Unknown provenance"; +"Unresolved operation: %@" = "Unresolved operation: %@"; +"Usage unavailable" = "Usage unavailable"; +"Use HTTPS, or loopback HTTP, without credentials, query parameters, or fragments." = "Use HTTPS, or loopback HTTP, without credentials, query parameters, or fragments."; +"Vendor-metered" = "Vendor-metered"; +"Waiting for CAAM; the app remains available." = "Waiting for CAAM; the app remains available."; +"Window length: %d minutes" = "Window length: %d minutes"; +"Work" = "Work"; diff --git a/Sources/CodexBar/Resources/uk.lproj/Localizable.strings b/Sources/CodexBar/Resources/uk.lproj/Localizable.strings index f1fb9053dc..2e20871cde 100644 --- a/Sources/CodexBar/Resources/uk.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/uk.lproj/Localizable.strings @@ -1562,3 +1562,78 @@ "SSH" = "SSH"; "SSH destination" = "Адреса SSH"; "Update required" = "Потрібне оновлення"; + +/* CAAM observability: English fallback pending translation review. */ +"%@ · Recorded session %d" = "%@ · Recorded session %d"; +"Affected profiles" = "Affected profiles"; +"An unresolved operation belongs to a changed environment. Restore its original configuration." = "An unresolved operation belongs to a changed environment. Restore its original configuration."; +"Available" = "Available"; +"CAAM may complete or roll back its pending transaction. This does not restore fallback." = "CAAM may complete or roll back its pending transaction. This does not restore fallback."; +"CAAM operation tracking is unavailable. Mutations remain disabled." = "CAAM operation tracking is unavailable. Mutations remain disabled."; +"CAAM requires manual intervention on this environment. Automatic recovery is disabled." = "CAAM requires manual intervention on this environment. Automatic recovery is disabled."; +"Change host default: %@ → %@" = "Change host default: %@ → %@"; +"Confirm once" = "Confirm once"; +"Cost unavailable; not zero." = "Cost unavailable; not zero."; +"Dashboard URL (not stored)" = "Dashboard URL (not stored)"; +"Data confidence: %@" = "Data confidence: %@"; +"Estimated API cost: %@" = "Estimated API cost: %@"; +"Expected revision: %@" = "Expected revision: %@"; +"Gateway code: %@" = "Gateway code: %@"; +"Historical provider cache; not a billing receipt or an identified CAAM account." = "Historical provider cache; not a billing receipt or an identified CAAM account."; +"Host default" = "Host default"; +"Host default: %@" = "Host default: %@"; +"Host resources" = "Host resources"; +"Last operation: %@ · %@" = "Last operation: %@ · %@"; +"List-price estimate" = "List-price estimate"; +"Local service state, provider usage and cost, and host resources are separate ledgers." = "Local service state, provider usage and cost, and host resources are separate ledgers."; +"Look up operation" = "Look up operation"; +"Mixed provenance" = "Mixed provenance"; +"Netdata telemetry is unsupported here. No host metrics were collected." = "Netdata telemetry is unsupported here. No host metrics were collected."; +"No account profiles have been observed." = "No account profiles have been observed."; +"No observation timestamp" = "No observation timestamp"; +"No provider observations are available." = "No provider observations are available."; +"No session details are loaded. Use the existing provider refresh to collect supported data." = "No session details are loaded. Use the existing provider refresh to collect supported data."; +"Observability hub" = "Observability hub"; +"Observability view" = "Observability view"; +"Observed: %@" = "Observed: %@"; +"Open an existing Netdata dashboard" = "Open an existing Netdata dashboard"; +"Open dashboard in browser" = "Open dashboard in browser"; +"Opening a dashboard does not import telemetry or verify this host." = "Opening a dashboard does not import telemetry or verify this host."; +"Operation outcome is unresolved. Look up the existing operation before another change." = "Operation outcome is unresolved. Look up the existing operation before another change."; +"Partial" = "Partial"; +"Partial view: limited to 100 recorded sessions." = "Partial view: limited to 100 recorded sessions."; +"Plan digest: %@" = "Plan digest: %@"; +"Plan fallback" = "Plan fallback"; +"Plan switch" = "Plan switch"; +"Primary window: %.1f%% used" = "Primary window: %.1f%% used"; +"Profiles are environment-local. Only provider and stable ID establish cross-environment identity." = "Profiles are environment-local. Only provider and stable ID establish cross-environment identity."; +"Provider usage and API cost" = "Provider usage and API cost"; +"Provider: %@ · Health: %@" = "Provider: %@ · Health: %@"; +"Read-only until CAAM conformance is independently qualified." = "Read-only until CAAM conformance is independently qualified."; +"Recorded cost: %@ · %d days · %@" = "Recorded cost: %@ · %d days · %@"; +"Recorded local sessions only; not a live job queue or evidence of running processes." = "Recorded local sessions only; not a live job queue or evidence of running processes."; +"Refresh and resolve pending operations before changing this environment." = "Refresh and resolve pending operations before changing this environment."; +"Resets: %@" = "Resets: %@"; +"Resolve retained operations before adding more environment observations." = "Resolve retained operations before adding more environment observations."; +"Retained unresolved operation" = "Retained unresolved operation"; +"Review fallback restoration" = "Review fallback restoration"; +"Review interrupted-operation recovery" = "Review interrupted-operation recovery"; +"Review recovery" = "Review recovery"; +"Review switch plan" = "Review switch plan"; +"Runtime effective profile: %@" = "Runtime effective profile: %@"; +"Runtime state: %@" = "Runtime state: %@"; +"Separate metered amount: %@ (not added)" = "Separate metered amount: %@ (not added)"; +"Service and local state" = "Service and local state"; +"Stale" = "Stale"; +"Status and recovery remain bound to the original environment configuration." = "Status and recovery remain bound to the original environment configuration."; +"Systems" = "Systems"; +"The CAAM configuration is invalid." = "The CAAM configuration is invalid."; +"Tokens: %d" = "Tokens: %d"; +"Unknown provenance" = "Unknown provenance"; +"Unresolved operation: %@" = "Unresolved operation: %@"; +"Usage unavailable" = "Usage unavailable"; +"Use HTTPS, or loopback HTTP, without credentials, query parameters, or fragments." = "Use HTTPS, or loopback HTTP, without credentials, query parameters, or fragments."; +"Vendor-metered" = "Vendor-metered"; +"Waiting for CAAM; the app remains available." = "Waiting for CAAM; the app remains available."; +"Window length: %d minutes" = "Window length: %d minutes"; +"Work" = "Work"; diff --git a/Sources/CodexBar/Resources/vi.lproj/Localizable.strings b/Sources/CodexBar/Resources/vi.lproj/Localizable.strings index 6e4840a5b0..bd56ce5540 100644 --- a/Sources/CodexBar/Resources/vi.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/vi.lproj/Localizable.strings @@ -1563,3 +1563,78 @@ "SSH" = "SSH"; "SSH destination" = "Đích SSH"; "Update required" = "Cần cập nhật"; + +/* CAAM observability: English fallback pending translation review. */ +"%@ · Recorded session %d" = "%@ · Recorded session %d"; +"Affected profiles" = "Affected profiles"; +"An unresolved operation belongs to a changed environment. Restore its original configuration." = "An unresolved operation belongs to a changed environment. Restore its original configuration."; +"Available" = "Available"; +"CAAM may complete or roll back its pending transaction. This does not restore fallback." = "CAAM may complete or roll back its pending transaction. This does not restore fallback."; +"CAAM operation tracking is unavailable. Mutations remain disabled." = "CAAM operation tracking is unavailable. Mutations remain disabled."; +"CAAM requires manual intervention on this environment. Automatic recovery is disabled." = "CAAM requires manual intervention on this environment. Automatic recovery is disabled."; +"Change host default: %@ → %@" = "Change host default: %@ → %@"; +"Confirm once" = "Confirm once"; +"Cost unavailable; not zero." = "Cost unavailable; not zero."; +"Dashboard URL (not stored)" = "Dashboard URL (not stored)"; +"Data confidence: %@" = "Data confidence: %@"; +"Estimated API cost: %@" = "Estimated API cost: %@"; +"Expected revision: %@" = "Expected revision: %@"; +"Gateway code: %@" = "Gateway code: %@"; +"Historical provider cache; not a billing receipt or an identified CAAM account." = "Historical provider cache; not a billing receipt or an identified CAAM account."; +"Host default" = "Host default"; +"Host default: %@" = "Host default: %@"; +"Host resources" = "Host resources"; +"Last operation: %@ · %@" = "Last operation: %@ · %@"; +"List-price estimate" = "List-price estimate"; +"Local service state, provider usage and cost, and host resources are separate ledgers." = "Local service state, provider usage and cost, and host resources are separate ledgers."; +"Look up operation" = "Look up operation"; +"Mixed provenance" = "Mixed provenance"; +"Netdata telemetry is unsupported here. No host metrics were collected." = "Netdata telemetry is unsupported here. No host metrics were collected."; +"No account profiles have been observed." = "No account profiles have been observed."; +"No observation timestamp" = "No observation timestamp"; +"No provider observations are available." = "No provider observations are available."; +"No session details are loaded. Use the existing provider refresh to collect supported data." = "No session details are loaded. Use the existing provider refresh to collect supported data."; +"Observability hub" = "Observability hub"; +"Observability view" = "Observability view"; +"Observed: %@" = "Observed: %@"; +"Open an existing Netdata dashboard" = "Open an existing Netdata dashboard"; +"Open dashboard in browser" = "Open dashboard in browser"; +"Opening a dashboard does not import telemetry or verify this host." = "Opening a dashboard does not import telemetry or verify this host."; +"Operation outcome is unresolved. Look up the existing operation before another change." = "Operation outcome is unresolved. Look up the existing operation before another change."; +"Partial" = "Partial"; +"Partial view: limited to 100 recorded sessions." = "Partial view: limited to 100 recorded sessions."; +"Plan digest: %@" = "Plan digest: %@"; +"Plan fallback" = "Plan fallback"; +"Plan switch" = "Plan switch"; +"Primary window: %.1f%% used" = "Primary window: %.1f%% used"; +"Profiles are environment-local. Only provider and stable ID establish cross-environment identity." = "Profiles are environment-local. Only provider and stable ID establish cross-environment identity."; +"Provider usage and API cost" = "Provider usage and API cost"; +"Provider: %@ · Health: %@" = "Provider: %@ · Health: %@"; +"Read-only until CAAM conformance is independently qualified." = "Read-only until CAAM conformance is independently qualified."; +"Recorded cost: %@ · %d days · %@" = "Recorded cost: %@ · %d days · %@"; +"Recorded local sessions only; not a live job queue or evidence of running processes." = "Recorded local sessions only; not a live job queue or evidence of running processes."; +"Refresh and resolve pending operations before changing this environment." = "Refresh and resolve pending operations before changing this environment."; +"Resets: %@" = "Resets: %@"; +"Resolve retained operations before adding more environment observations." = "Resolve retained operations before adding more environment observations."; +"Retained unresolved operation" = "Retained unresolved operation"; +"Review fallback restoration" = "Review fallback restoration"; +"Review interrupted-operation recovery" = "Review interrupted-operation recovery"; +"Review recovery" = "Review recovery"; +"Review switch plan" = "Review switch plan"; +"Runtime effective profile: %@" = "Runtime effective profile: %@"; +"Runtime state: %@" = "Runtime state: %@"; +"Separate metered amount: %@ (not added)" = "Separate metered amount: %@ (not added)"; +"Service and local state" = "Service and local state"; +"Stale" = "Stale"; +"Status and recovery remain bound to the original environment configuration." = "Status and recovery remain bound to the original environment configuration."; +"Systems" = "Systems"; +"The CAAM configuration is invalid." = "The CAAM configuration is invalid."; +"Tokens: %d" = "Tokens: %d"; +"Unknown provenance" = "Unknown provenance"; +"Unresolved operation: %@" = "Unresolved operation: %@"; +"Usage unavailable" = "Usage unavailable"; +"Use HTTPS, or loopback HTTP, without credentials, query parameters, or fragments." = "Use HTTPS, or loopback HTTP, without credentials, query parameters, or fragments."; +"Vendor-metered" = "Vendor-metered"; +"Waiting for CAAM; the app remains available." = "Waiting for CAAM; the app remains available."; +"Window length: %d minutes" = "Window length: %d minutes"; +"Work" = "Work"; diff --git a/Sources/CodexBar/Resources/zh-Hans.lproj/Localizable.strings b/Sources/CodexBar/Resources/zh-Hans.lproj/Localizable.strings index 95da4b6866..83fc92038e 100644 --- a/Sources/CodexBar/Resources/zh-Hans.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/zh-Hans.lproj/Localizable.strings @@ -1574,3 +1574,78 @@ "SSH" = "SSH"; "SSH destination" = "SSH 目标"; "Update required" = "需要更新"; + +/* CAAM observability: English fallback pending translation review. */ +"%@ · Recorded session %d" = "%@ · Recorded session %d"; +"Affected profiles" = "Affected profiles"; +"An unresolved operation belongs to a changed environment. Restore its original configuration." = "An unresolved operation belongs to a changed environment. Restore its original configuration."; +"Available" = "Available"; +"CAAM may complete or roll back its pending transaction. This does not restore fallback." = "CAAM may complete or roll back its pending transaction. This does not restore fallback."; +"CAAM operation tracking is unavailable. Mutations remain disabled." = "CAAM operation tracking is unavailable. Mutations remain disabled."; +"CAAM requires manual intervention on this environment. Automatic recovery is disabled." = "CAAM requires manual intervention on this environment. Automatic recovery is disabled."; +"Change host default: %@ → %@" = "Change host default: %@ → %@"; +"Confirm once" = "Confirm once"; +"Cost unavailable; not zero." = "Cost unavailable; not zero."; +"Dashboard URL (not stored)" = "Dashboard URL (not stored)"; +"Data confidence: %@" = "Data confidence: %@"; +"Estimated API cost: %@" = "Estimated API cost: %@"; +"Expected revision: %@" = "Expected revision: %@"; +"Gateway code: %@" = "Gateway code: %@"; +"Historical provider cache; not a billing receipt or an identified CAAM account." = "Historical provider cache; not a billing receipt or an identified CAAM account."; +"Host default" = "Host default"; +"Host default: %@" = "Host default: %@"; +"Host resources" = "Host resources"; +"Last operation: %@ · %@" = "Last operation: %@ · %@"; +"List-price estimate" = "List-price estimate"; +"Local service state, provider usage and cost, and host resources are separate ledgers." = "Local service state, provider usage and cost, and host resources are separate ledgers."; +"Look up operation" = "Look up operation"; +"Mixed provenance" = "Mixed provenance"; +"Netdata telemetry is unsupported here. No host metrics were collected." = "Netdata telemetry is unsupported here. No host metrics were collected."; +"No account profiles have been observed." = "No account profiles have been observed."; +"No observation timestamp" = "No observation timestamp"; +"No provider observations are available." = "No provider observations are available."; +"No session details are loaded. Use the existing provider refresh to collect supported data." = "No session details are loaded. Use the existing provider refresh to collect supported data."; +"Observability hub" = "Observability hub"; +"Observability view" = "Observability view"; +"Observed: %@" = "Observed: %@"; +"Open an existing Netdata dashboard" = "Open an existing Netdata dashboard"; +"Open dashboard in browser" = "Open dashboard in browser"; +"Opening a dashboard does not import telemetry or verify this host." = "Opening a dashboard does not import telemetry or verify this host."; +"Operation outcome is unresolved. Look up the existing operation before another change." = "Operation outcome is unresolved. Look up the existing operation before another change."; +"Partial" = "Partial"; +"Partial view: limited to 100 recorded sessions." = "Partial view: limited to 100 recorded sessions."; +"Plan digest: %@" = "Plan digest: %@"; +"Plan fallback" = "Plan fallback"; +"Plan switch" = "Plan switch"; +"Primary window: %.1f%% used" = "Primary window: %.1f%% used"; +"Profiles are environment-local. Only provider and stable ID establish cross-environment identity." = "Profiles are environment-local. Only provider and stable ID establish cross-environment identity."; +"Provider usage and API cost" = "Provider usage and API cost"; +"Provider: %@ · Health: %@" = "Provider: %@ · Health: %@"; +"Read-only until CAAM conformance is independently qualified." = "Read-only until CAAM conformance is independently qualified."; +"Recorded cost: %@ · %d days · %@" = "Recorded cost: %@ · %d days · %@"; +"Recorded local sessions only; not a live job queue or evidence of running processes." = "Recorded local sessions only; not a live job queue or evidence of running processes."; +"Refresh and resolve pending operations before changing this environment." = "Refresh and resolve pending operations before changing this environment."; +"Resets: %@" = "Resets: %@"; +"Resolve retained operations before adding more environment observations." = "Resolve retained operations before adding more environment observations."; +"Retained unresolved operation" = "Retained unresolved operation"; +"Review fallback restoration" = "Review fallback restoration"; +"Review interrupted-operation recovery" = "Review interrupted-operation recovery"; +"Review recovery" = "Review recovery"; +"Review switch plan" = "Review switch plan"; +"Runtime effective profile: %@" = "Runtime effective profile: %@"; +"Runtime state: %@" = "Runtime state: %@"; +"Separate metered amount: %@ (not added)" = "Separate metered amount: %@ (not added)"; +"Service and local state" = "Service and local state"; +"Stale" = "Stale"; +"Status and recovery remain bound to the original environment configuration." = "Status and recovery remain bound to the original environment configuration."; +"Systems" = "Systems"; +"The CAAM configuration is invalid." = "The CAAM configuration is invalid."; +"Tokens: %d" = "Tokens: %d"; +"Unknown provenance" = "Unknown provenance"; +"Unresolved operation: %@" = "Unresolved operation: %@"; +"Usage unavailable" = "Usage unavailable"; +"Use HTTPS, or loopback HTTP, without credentials, query parameters, or fragments." = "Use HTTPS, or loopback HTTP, without credentials, query parameters, or fragments."; +"Vendor-metered" = "Vendor-metered"; +"Waiting for CAAM; the app remains available." = "Waiting for CAAM; the app remains available."; +"Window length: %d minutes" = "Window length: %d minutes"; +"Work" = "Work"; diff --git a/Sources/CodexBar/Resources/zh-Hant.lproj/Localizable.strings b/Sources/CodexBar/Resources/zh-Hant.lproj/Localizable.strings index 7529578074..16da74782c 100644 --- a/Sources/CodexBar/Resources/zh-Hant.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/zh-Hant.lproj/Localizable.strings @@ -1593,3 +1593,78 @@ "SSH" = "SSH"; "SSH destination" = "SSH 目的地"; "Update required" = "需要更新"; + +/* CAAM observability: English fallback pending translation review. */ +"%@ · Recorded session %d" = "%@ · Recorded session %d"; +"Affected profiles" = "Affected profiles"; +"An unresolved operation belongs to a changed environment. Restore its original configuration." = "An unresolved operation belongs to a changed environment. Restore its original configuration."; +"Available" = "Available"; +"CAAM may complete or roll back its pending transaction. This does not restore fallback." = "CAAM may complete or roll back its pending transaction. This does not restore fallback."; +"CAAM operation tracking is unavailable. Mutations remain disabled." = "CAAM operation tracking is unavailable. Mutations remain disabled."; +"CAAM requires manual intervention on this environment. Automatic recovery is disabled." = "CAAM requires manual intervention on this environment. Automatic recovery is disabled."; +"Change host default: %@ → %@" = "Change host default: %@ → %@"; +"Confirm once" = "Confirm once"; +"Cost unavailable; not zero." = "Cost unavailable; not zero."; +"Dashboard URL (not stored)" = "Dashboard URL (not stored)"; +"Data confidence: %@" = "Data confidence: %@"; +"Estimated API cost: %@" = "Estimated API cost: %@"; +"Expected revision: %@" = "Expected revision: %@"; +"Gateway code: %@" = "Gateway code: %@"; +"Historical provider cache; not a billing receipt or an identified CAAM account." = "Historical provider cache; not a billing receipt or an identified CAAM account."; +"Host default" = "Host default"; +"Host default: %@" = "Host default: %@"; +"Host resources" = "Host resources"; +"Last operation: %@ · %@" = "Last operation: %@ · %@"; +"List-price estimate" = "List-price estimate"; +"Local service state, provider usage and cost, and host resources are separate ledgers." = "Local service state, provider usage and cost, and host resources are separate ledgers."; +"Look up operation" = "Look up operation"; +"Mixed provenance" = "Mixed provenance"; +"Netdata telemetry is unsupported here. No host metrics were collected." = "Netdata telemetry is unsupported here. No host metrics were collected."; +"No account profiles have been observed." = "No account profiles have been observed."; +"No observation timestamp" = "No observation timestamp"; +"No provider observations are available." = "No provider observations are available."; +"No session details are loaded. Use the existing provider refresh to collect supported data." = "No session details are loaded. Use the existing provider refresh to collect supported data."; +"Observability hub" = "Observability hub"; +"Observability view" = "Observability view"; +"Observed: %@" = "Observed: %@"; +"Open an existing Netdata dashboard" = "Open an existing Netdata dashboard"; +"Open dashboard in browser" = "Open dashboard in browser"; +"Opening a dashboard does not import telemetry or verify this host." = "Opening a dashboard does not import telemetry or verify this host."; +"Operation outcome is unresolved. Look up the existing operation before another change." = "Operation outcome is unresolved. Look up the existing operation before another change."; +"Partial" = "Partial"; +"Partial view: limited to 100 recorded sessions." = "Partial view: limited to 100 recorded sessions."; +"Plan digest: %@" = "Plan digest: %@"; +"Plan fallback" = "Plan fallback"; +"Plan switch" = "Plan switch"; +"Primary window: %.1f%% used" = "Primary window: %.1f%% used"; +"Profiles are environment-local. Only provider and stable ID establish cross-environment identity." = "Profiles are environment-local. Only provider and stable ID establish cross-environment identity."; +"Provider usage and API cost" = "Provider usage and API cost"; +"Provider: %@ · Health: %@" = "Provider: %@ · Health: %@"; +"Read-only until CAAM conformance is independently qualified." = "Read-only until CAAM conformance is independently qualified."; +"Recorded cost: %@ · %d days · %@" = "Recorded cost: %@ · %d days · %@"; +"Recorded local sessions only; not a live job queue or evidence of running processes." = "Recorded local sessions only; not a live job queue or evidence of running processes."; +"Refresh and resolve pending operations before changing this environment." = "Refresh and resolve pending operations before changing this environment."; +"Resets: %@" = "Resets: %@"; +"Resolve retained operations before adding more environment observations." = "Resolve retained operations before adding more environment observations."; +"Retained unresolved operation" = "Retained unresolved operation"; +"Review fallback restoration" = "Review fallback restoration"; +"Review interrupted-operation recovery" = "Review interrupted-operation recovery"; +"Review recovery" = "Review recovery"; +"Review switch plan" = "Review switch plan"; +"Runtime effective profile: %@" = "Runtime effective profile: %@"; +"Runtime state: %@" = "Runtime state: %@"; +"Separate metered amount: %@ (not added)" = "Separate metered amount: %@ (not added)"; +"Service and local state" = "Service and local state"; +"Stale" = "Stale"; +"Status and recovery remain bound to the original environment configuration." = "Status and recovery remain bound to the original environment configuration."; +"Systems" = "Systems"; +"The CAAM configuration is invalid." = "The CAAM configuration is invalid."; +"Tokens: %d" = "Tokens: %d"; +"Unknown provenance" = "Unknown provenance"; +"Unresolved operation: %@" = "Unresolved operation: %@"; +"Usage unavailable" = "Usage unavailable"; +"Use HTTPS, or loopback HTTP, without credentials, query parameters, or fragments." = "Use HTTPS, or loopback HTTP, without credentials, query parameters, or fragments."; +"Vendor-metered" = "Vendor-metered"; +"Waiting for CAAM; the app remains available." = "Waiting for CAAM; the app remains available."; +"Window length: %d minutes" = "Window length: %d minutes"; +"Work" = "Work"; diff --git a/Sources/CodexBarCore/Observability/NetdataReadOnlyFacade.swift b/Sources/CodexBarCore/Observability/NetdataReadOnlyFacade.swift index 007a4fa42f..1d41b65b04 100644 --- a/Sources/CodexBarCore/Observability/NetdataReadOnlyFacade.swift +++ b/Sources/CodexBarCore/Observability/NetdataReadOnlyFacade.swift @@ -14,7 +14,8 @@ public enum NetdataReadOnlyFacade { public static func unavailable(environmentID: String) -> HubHostObservation { HubHostObservation( environmentID: environmentID, - evidence: HubEvidence(source: "Netdata collector not configured", observedAt: nil, availability: .unsupported), + evidence: HubEvidence( + source: "Netdata collector not configured", observedAt: nil, availability: .unsupported), cpuPercent: nil, memoryUsedBytes: nil) } diff --git a/Sources/CodexBarCore/Observability/ObservabilityHub.swift b/Sources/CodexBarCore/Observability/ObservabilityHub.swift index 32d52a76ec..99b2df8bd6 100644 --- a/Sources/CodexBarCore/Observability/ObservabilityHub.swift +++ b/Sources/CodexBarCore/Observability/ObservabilityHub.swift @@ -32,7 +32,8 @@ public struct HubEvidence: Sendable, Equatable { } let age = now.timeIntervalSince(observedAt) let stale = failed || !age.isFinite || age < -5 || age > 300 - return Self(source: source, observedAt: observedAt, availability: stale ? .stale : partial ? .partial : .available) + return Self( + source: source, observedAt: observedAt, availability: stale ? .stale : partial ? .partial : .available) } } diff --git a/Tests/CodexBarTests/CAAMEnvironmentCoordinatorTests.swift b/Tests/CodexBarTests/CAAMEnvironmentCoordinatorTests.swift new file mode 100644 index 0000000000..6dcf451423 --- /dev/null +++ b/Tests/CodexBarTests/CAAMEnvironmentCoordinatorTests.swift @@ -0,0 +1,200 @@ +import Foundation +import Testing +@testable import CodexBar +@testable import CodexBarCore + +@MainActor +struct CAAMEnvironmentCoordinatorTests { + @Test + func `refresh coalesces and never publishes a response into changed configuration`() async { + let runner = GatedCAAMCoordinatorRunner() + let coordinator = Self.coordinator(runner: runner) + let configuration = CAAMCoordinatorFixture.configuration + let task = Task { await coordinator.refresh(configurations: [configuration]) } + await runner.waitForStart() + #expect(coordinator.isRefreshing) + await coordinator.refresh(configurations: [configuration]) + #expect(await runner.commandCount == 1) + let changed = CAAMEnvironmentConfiguration( + id: configuration.id, label: "Changed", connection: .ssh(destination: "changed-fixture")) + coordinator.configurationsDidChange([changed]) + await runner.release() + await task.value + #expect(!coordinator.isRefreshing) + #expect(coordinator.state(configurations: [changed]).rows.first?.availability == .unavailable) + #expect(coordinator.sessionsByID.isEmpty) + } + + @Test + func `production qualification default rejects execute even with all advertised capabilities`() async throws { + let runner = CAAMCoordinatorFixtureRunner() + let coordinator = Self.coordinator(runner: runner) + let configuration = CAAMCoordinatorFixture.configuration + await coordinator.refresh(configurations: [configuration]) + await coordinator.plan(configuration: configuration, target: "secondary") + let confirmation = try #require(coordinator.session(for: configuration).confirmation) + await coordinator.confirm(configuration: configuration, confirmationID: confirmation.id) + #expect(await runner.operations == ["snapshot", "plan-switch"]) + #expect(!coordinator.mutationsQualified(for: configuration)) + #expect(coordinator.notice == CAAMControlFailure(reason: .notQualified).localizedDescription) + } + + @Test + func `operation identity persists before launch and restart resolves without replay`() async throws { + let runner = CAAMCoordinatorFixtureRunner() + let receiptStore = CAAMMemoryReceiptStore() + let configuration = CAAMCoordinatorFixture.configuration + let coordinator = Self.coordinator(runner: runner, receiptStore: receiptStore, qualified: [configuration]) + await coordinator.refresh(configurations: [configuration]) + await coordinator.plan(configuration: configuration, target: "secondary") + let confirmation = try #require(coordinator.session(for: configuration).confirmation) + await coordinator.confirm(configuration: configuration, confirmationID: confirmation.id) + let receipt = try #require(try await receiptStore.load().first) + #expect(coordinator.session(for: configuration).phase == .unknown) + #expect(!coordinator.canChangeConfigurations(from: [configuration], to: [])) + let restarted = Self.coordinator(runner: runner, receiptStore: receiptStore) + await restarted.refresh(configurations: [configuration]) + #expect(restarted.session(for: configuration).pendingOperationID == receipt.operationID) + #expect(restarted.session(for: configuration).phase == .unknown) + await restarted.lookup(configuration: configuration) + #expect(restarted.session(for: configuration).phase == .finished) + #expect(try await receiptStore.load().isEmpty) + #expect(await runner.operations == [ + "snapshot", "plan-switch", "execute-switch", "snapshot", "operation-status", + ]) + } + + @Test + func `failed receipt persistence prevents launch and disables mutation qualification`() async throws { + let runner = CAAMCoordinatorFixtureRunner() + let configuration = CAAMCoordinatorFixture.configuration + let coordinator = Self.coordinator( + runner: runner, receiptStore: RejectingCAAMReceiptStore(), qualified: [configuration]) + await coordinator.refresh(configurations: [configuration]) + await coordinator.plan(configuration: configuration, target: "secondary") + let confirmation = try #require(coordinator.session(for: configuration).confirmation) + await coordinator.confirm(configuration: configuration, confirmationID: confirmation.id) + #expect(await runner.operations == ["snapshot", "plan-switch"]) + #expect(!coordinator.receiptStorageAvailable) + #expect(!coordinator.mutationsQualified(for: configuration)) + #expect(!coordinator.canChangeConfigurations(from: [configuration], to: [])) + } + + @Test + func `qualification binds full configuration and retained operations keep their original target`() async throws { + let configuration = CAAMCoordinatorFixture.configuration + let receipt = CAAMOperationReceipt(configuration: configuration, operationID: UUID()) + let store = CAAMMemoryReceiptStore(receipts: [receipt]) + let runner = CAAMCoordinatorFixtureRunner() + let coordinator = Self.coordinator(runner: runner, receiptStore: store, qualified: [configuration]) + await coordinator.refresh(configurations: []) + let changed = CAAMEnvironmentConfiguration( + id: configuration.id, label: "Changed", connection: .ssh(destination: "other-fixture")) + #expect(!coordinator.mutationsQualified(for: changed)) + #expect(coordinator.retainedConfigurations(outside: [changed]) == [configuration]) + #expect(coordinator.session(for: configuration).pendingOperationID == receipt.operationID) + #expect(coordinator.session(for: changed).pendingOperationID == nil) + await coordinator.lookup(configuration: configuration) + #expect(try await store.load().isEmpty) + #expect(await runner.operations == ["operation-status"]) + } + + private static func coordinator( + runner: any CAAMEnvironmentCommandRunning, + receiptStore: any CAAMOperationReceiptStoring = CAAMMemoryReceiptStore(), + qualified: [CAAMEnvironmentConfiguration] = []) -> CAAMEnvironmentCoordinator + { + CAAMEnvironmentCoordinator( + client: CAAMEnvironmentClient( + runner: runner, environment: [:], resolveLocalGateway: { _ in "/fixture/caam-codexbar" }), + receiptStore: receiptStore, + qualifiedConfigurations: qualified, + now: { CAAMCoordinatorFixture.now }) + } +} + +private enum CAAMCoordinatorFixture { + static let now = Date(timeIntervalSince1970: 1_700_000_000) + static let configuration = CAAMEnvironmentConfiguration(id: "fixture", label: "Fixture", connection: .local()) + + static func snapshot(active: String = "primary") -> CAAMEnvironmentSnapshot { + CAAMEnvironmentSnapshot( + revision: "1", observedAt: Self.now, caamVersion: "fixture", + capabilities: CAAMControlCapability.allCases.map(\.rawValue), reachability: .reachable, + hostDefaultProfile: active, fallbackProfile: "secondary", + profiles: ["primary", "secondary"].map { name in + CAAMEnvironmentProfile( + name: name, active: name == active, system: false, eligible: true, health: .healthy) + }, + runtime: CAAMRuntimeSnapshot(state: .unknown, reloadRequired: false)) + } + + static func response(_ value: Value, kind: String) throws -> SubprocessResult { + let encoder = JSONEncoder() + encoder.dateEncodingStrategy = .iso8601 + let data = try encoder.encode(CAAMControlEnvelope( + schema: CAAMEnvironmentContract.schema, kind: kind, environmentID: Self.configuration.id, + protocolVersion: "1.0", result: value, error: nil)) + return SubprocessResult(stdout: String(decoding: data, as: UTF8.self), stderr: "") + } +} + +private actor CAAMCoordinatorFixtureRunner: CAAMEnvironmentCommandRunning { + private(set) var operations: [String] = [] + + func run(command: CAAMEnvironmentCommand, environment _: [String: String]) throws -> SubprocessResult { + let kind = command.arguments.first ?? "" + self.operations.append(kind) + switch kind { + case "snapshot": + return try CAAMCoordinatorFixture.response(CAAMCoordinatorFixture.snapshot(), kind: kind) + case "plan-switch": + return try CAAMCoordinatorFixture.response(CAAMSwitchPlan( + planDigest: String(repeating: "a", count: 64), expectedRevision: "1", currentProfile: "primary", + targetProfile: "secondary", affectedProfiles: ["primary", "secondary"], reloadRequired: false, + expiresAt: CAAMCoordinatorFixture.now.addingTimeInterval(30)), kind: kind) + case "execute-switch": + throw SubprocessRunnerError.timedOut("fixture interruption") + case "operation-status": + guard let operationID = UUID(uuidString: command.arguments.last ?? "") else { + throw SubprocessRunnerError.launchFailed("fixture invalid operation ID") + } + return try CAAMCoordinatorFixture.response(CAAMOperationResult( + operationID: operationID, state: .committed, effect: .knownEffect, + previousProfile: "primary", currentProfile: "secondary", + snapshot: CAAMCoordinatorFixture.snapshot(active: "secondary")), kind: kind) + default: + throw SubprocessRunnerError.launchFailed("fixture unexpected operation") + } + } +} + +private actor GatedCAAMCoordinatorRunner: CAAMEnvironmentCommandRunning { + private var startWaiter: CheckedContinuation? + private var releaseWaiter: CheckedContinuation? + private(set) var commandCount = 0 + + func waitForStart() async { + if self.commandCount > 0 { return } + await withCheckedContinuation { self.startWaiter = $0 } + } + + func release() { + self.releaseWaiter?.resume() + self.releaseWaiter = nil + } + + func run(command _: CAAMEnvironmentCommand, environment _: [String: String]) async throws -> SubprocessResult { + self.commandCount += 1 + self.startWaiter?.resume() + self.startWaiter = nil + await withCheckedContinuation { self.releaseWaiter = $0 } + return try CAAMCoordinatorFixture.response(CAAMCoordinatorFixture.snapshot(), kind: "snapshot") + } +} + +private actor RejectingCAAMReceiptStore: CAAMOperationReceiptStoring { + func load() -> [CAAMOperationReceipt] { [] } + + func save(_: [CAAMOperationReceipt]) throws { throw CAAMControlFailure(reason: .unavailable) } +} diff --git a/Tests/CodexBarTests/CAAMEnvironmentSettingsTests.swift b/Tests/CodexBarTests/CAAMEnvironmentSettingsTests.swift index 0e79ddbffc..47e8317069 100644 --- a/Tests/CodexBarTests/CAAMEnvironmentSettingsTests.swift +++ b/Tests/CodexBarTests/CAAMEnvironmentSettingsTests.swift @@ -11,7 +11,7 @@ struct CAAMEnvironmentSettingsTests { let settings = Self.makeSettingsStore(suite: suite) defer { Self.clearSettingsStore(settings, suite: suite) } let store = Self.makeUsageStore(settings: settings) - let pane = ProvidersPane(settings: settings, store: store) + let pane = ProvidersPane(settings: settings, store: store, caamEnvironmentCoordinator: CAAMEnvironmentCoordinator()) let state = pane._test_codexEnvironmentsSectionState() @@ -26,7 +26,7 @@ struct CAAMEnvironmentSettingsTests { let settings = Self.makeSettingsStore(suite: suite) defer { Self.clearSettingsStore(settings, suite: suite) } let store = Self.makeUsageStore(settings: settings) - let pane = ProvidersPane(settings: settings, store: store) + let pane = ProvidersPane(settings: settings, store: store, caamEnvironmentCoordinator: CAAMEnvironmentCoordinator()) let configurations = [ CAAMEnvironmentConfiguration(id: "vps", label: "VPS", connection: .ssh(destination: "vps")), CAAMEnvironmentConfiguration(id: "laptop", label: "Laptop", connection: .local()), @@ -49,7 +49,7 @@ struct CAAMEnvironmentSettingsTests { let settings = Self.makeSettingsStore(suite: suite) defer { Self.clearSettingsStore(settings, suite: suite) } let store = Self.makeUsageStore(settings: settings) - let pane = ProvidersPane(settings: settings, store: store) + let pane = ProvidersPane(settings: settings, store: store, caamEnvironmentCoordinator: CAAMEnvironmentCoordinator()) let unsafe = [ CAAMEnvironmentConfiguration( id: "vps", @@ -64,12 +64,14 @@ struct CAAMEnvironmentSettingsTests { } @Test - func `coordinator refresh projects a bounded snapshot`() async { + func `coordinator refresh projects a bounded snapshot`() async throws { let runner = CAAMSettingsTestRunner(stdout: Self.snapshotJSON) let client = CAAMEnvironmentClient( runner: runner, resolveLocalGateway: { _ in "/usr/local/bin/caam-codexbar" }) - let coordinator = CAAMEnvironmentCoordinator(client: client) + let observedAt = try CAAMEnvironmentContract.decodeSnapshot( + Data(Self.snapshotJSON.utf8), expectedEnvironmentID: "laptop").observedAt + let coordinator = CAAMEnvironmentCoordinator(client: client, now: { observedAt }) let configuration = CAAMEnvironmentConfiguration( id: "laptop", label: "Laptop", diff --git a/TestsLinux/CAAMControlFixtures.swift b/TestsLinux/CAAMControlFixtures.swift index 3efe7f2599..73b49ffc7e 100644 --- a/TestsLinux/CAAMControlFixtures.swift +++ b/TestsLinux/CAAMControlFixtures.swift @@ -23,7 +23,8 @@ enum CAAMControlFixtures { profiles: ["primary", "secondary"].map { name in CAAMEnvironmentProfile( name: name, active: name == active, system: false, eligible: true, health: .healthy, - identity: CAAMProfileIdentity(provider: provider, stableID: stableID, displayEmail: "f***@example.invalid")) + identity: CAAMProfileIdentity( + provider: provider, stableID: stableID, displayEmail: "f***@example.invalid")) }, runtime: CAAMRuntimeSnapshot(state: .unknown, reloadRequired: false), pendingOperation: pending, diff --git a/TestsLinux/ObservabilityHubLinuxTests.swift b/TestsLinux/ObservabilityHubLinuxTests.swift index fbe75b6e6b..03c409dfd7 100644 --- a/TestsLinux/ObservabilityHubLinuxTests.swift +++ b/TestsLinux/ObservabilityHubLinuxTests.swift @@ -81,7 +81,8 @@ struct ObservabilityHubLinuxTests { @Test func `account correlation requires matching provider and stable ID not profile name or email`() throws { let first = CAAMControlFixtures.session() - let otherConfiguration = CAAMEnvironmentConfiguration(id: "other", label: "Other", connection: .ssh(destination: "fixture")) + let otherConfiguration = CAAMEnvironmentConfiguration( + id: "other", label: "Other", connection: .ssh(destination: "fixture")) var other = CAAMControlSession(configuration: otherConfiguration) other.observe(CAAMControlFixtures.snapshot(provider: "other-provider"), now: CAAMControlFixtures.now) let hub = ObservabilityHub.aggregate(sessions: [first, other], providers: [], now: CAAMControlFixtures.now) diff --git a/docs/engineering-specs/caam-environment-control.md b/docs/engineering-specs/caam-environment-control.md index 6eb94f7f43..fdf845e1c5 100644 --- a/docs/engineering-specs/caam-environment-control.md +++ b/docs/engineering-specs/caam-environment-control.md @@ -18,7 +18,11 @@ Consumers: CodexBar implementation, CAAM gateway implementation, reviewers, veri Authority effect: none -Specification revision: 1 +Specification revision: 2 + +Revision 2 adds the combined control workflow and observability-hub candidate below. The +first-PR and prior merge language is historical; the current owner authorization permits +one draft PR to `Jones-Systems/CodexBar:main`, no merge and no live account operation. ## Outcome, scope, and finish line @@ -280,3 +284,100 @@ The first PR may contain planning artifacts, a versioned inactive protocol, envi The initial publication request authorized creating/using a task fork, committing the declared paths, refreshing and forward-merging canonical upstream main, pushing the exact task branch normally, and opening one draft PR. The owner's later merge request authorizes merging PR #1 into `Jones-Systems/CodexBar:main` after its required checks pass. Neither request authorizes upstream submission to `steipete/CodexBar`, signing, notarization, release, Homebrew publication, device installation, SSH gateway provisioning, live credential access, live account switching, daemon termination, or automatic software update. + + +## Combined controls and observability candidate + +The accepted [v1 control contract](../contracts/caam-codexbar-control-v1.md) remains the wire authority. +The read-only dependency is `Jones-Systems/CAAM` at +`work/caam-codexbar-gateway@8fdf44a1c2c3af0d36b934d79725608945790f32`. +Its implementation was not accessible during this pass; capability advertisement is not +independent conformance evidence. Production mutation qualification therefore defaults to an +empty set of exact environment configurations. There is no settings switch that bypasses this +qualification. Read-only discovery, plans, status lookup, model/UI work, fixtures, and caching +remain independently useful. The candidate does not claim full-project activation acceptance. + +### Control state and confirmation boundaries + +`CAAMControlSession` owns a pure state machine, separate from transport and SwiftUI. Plans bind +the environment, source revision/current profile, eligible target, affected profiles, digest, +reload consequence, and expiry. Confirming consumes a unique confirmation once; execution +receives a new operation UUID and idempotency UUID. Fallback and interrupted-operation recovery +have separate confirmation kinds and typed commands. Recovery retains the original operation UUID. + +Unknown mutation effects remain unresolved through refresh and application restart. A snapshot +with an empty pending journal does not prove a locally ambiguous operation completed. Only a +bound, semantically valid terminal operation result with a fresh postcondition snapshot can settle +it. A failed status or recovery request cannot erase the original operation. Manual-required +state disables automatic recovery. A rollback is reported as rollback, not successful switching. + +The application coordinator is a single UI-actor instance with sequential, bounded refresh and +operation coordination. It publishes each completed environment independently, coalesces refresh +requests, and rejects late results from a changed configuration generation. It does not block +the application waiting for subprocess completion. Intent receipts are persisted on a separate +actor before any possible mutation launch, with only validated configuration selectors and UUIDs. +The bounded atomic receipt file is private to the user (0600; newly created parent 0700). +Failed persistence disables mutations; unresolved records remain available for lookup against +their original configuration even if the configured environment list changes externally. + +The transport preserves native exit status and UTF-8 validity. A valid error envelope on nonzero +exit can establish a typed effect; a success envelope on nonzero exit cannot. Timeout, cancellation, +oversize, invalid encoding, and invalid responses after possible launch remain unknown-effect. +UI errors use closed descriptions and validated codes, never raw stdout, stderr, or remote messages. + +### Cache and freshness policy + +CAAM mutation evidence expires after 60 seconds, with a maximum five-second future-clock allowance. +Successful snapshots remain displayable for at most 15 minutes, measured against both their source +timestamp and receipt time. Refresh failure marks retained values stale rather than fresh or zero. +No background poll is added: configured environments refresh on section appearance when needed, +or explicitly on user request. Fifteen-second UI timeline ticks update presentation age only. + +Environment configuration remains bounded to 32 records and each snapshot to 64 profiles and +64 KiB. The coordinator bounds retained observations and does not silently evict an unresolved +operation. Full configuration identity, not a reused ID, binds cached evidence and qualification. + +### Observability ledgers and views + +`ObservabilityHub` aggregates already-published local values. It never performs a network request, +credential lookup, session scan, account switch, or host-process inspection. There is deliberately +no combined utilization or spend total across unlike measurements. + +| View | Evidence shown | Boundary | +| --- | --- | --- | +| Overview | CAAM service state; provider quota windows, confidence, source/time; separate cost and metered amounts | Preserve currency, window and estimate/vendor/mixed provenance; unavailable is not zero | +| Accounts | Environment-local profiles and redacted identities with health/source/time | Only matching provider and stable ID establish cross-environment identity; no email/name join | +| Systems | Host-default versus runtime-effective profile and recovery state | Unknown runtime remains unknown; host-resource ledger is separate | +| Work | At most 100 already-loaded local session records, materialized only on this tab | Historical partial evidence, never a live job queue; no private session IDs or paths in the view | + +Provider inputs are bounded to 64. Provider cache timestamps older than five minutes or following +a refresh failure are labeled stale. Synthetic placeholder quota values and non-finite/negative +numeric costs are unavailable. Existing historical cost publications are not attributed to a CAAM +account and are not presented as billing receipts. The hub avoids display helpers that can resolve +disk-backed identity or cookie caches. Current provider-configuration publication revision is checked +before admitting cost records; provider identity and account control remain separate authorities. + +### Netdata boundary + +No Netdata endpoint contract, configured collector, or response fixture exists in this tree. +`NetdataReadOnlyFacade` therefore reports unsupported host telemetry, with no observation timestamp +and no fabricated CPU/memory values. Systems offers a user-entered dashboard link, never stored or +prefilled from guessed host data. URLs require HTTPS, or HTTP on a literal loopback target; credentials, +query parameters, fragments, control characters, invalid ports, and other schemes are rejected. +Opening that link neither imports telemetry nor verifies host identity. No installation or Netdata +service modification is part of this candidate. + +### Candidate verification and residual gates + +New fixture suites cover stale/capability gating, plan expiry and confirmation reuse, exact response +binding, nonzero typed errors, invalid UTF-8, bounded output, unknown-effect lookup, recovery/fallback +separation, receipt restart/failure behavior, lazy aggregation, provenance, and safe dashboard links. +App tests exercise coalescing and late-result suppression through synthetic runners and fixed clocks. +Implicit CAAM subprocesses and real receipt-store access are disabled under existing test-process detection. + +The [single evolving Work Note](../work-notes/caam-environment-control.md) records actual check outcomes, +sequential self-review dispositions, incidents, and publication evidence. Source inspection and portable +catalog checks do not replace Swift compilation, native tests, or macOS UI proof. English fallback keys +are explicitly marked pending linguistic review. External CAAM conformance/activation, Netdata collector +qualification, and native platform evidence remain separate gates; none authorizes a live mutation here. + diff --git a/docs/work-notes/caam-environment-control.md b/docs/work-notes/caam-environment-control.md index d0471bdab4..3e9027ff26 100644 --- a/docs/work-notes/caam-environment-control.md +++ b/docs/work-notes/caam-environment-control.md @@ -135,6 +135,9 @@ The historical foundation evidence above is not evidence for this new candidate. Native Swift checks were already unavailable independently; implementation and publication are unaffected. - Resolution: fallback inspection pending confirmation. Next diagnostic: command availability in the same bound workspace, without changing PATH, installing utilities, or inspecting credentials. +- Resolution evidence: `git grep` completed the full in-scope long-line audit with exit 0. Standard Git, + grep, sed, and Python resolved successfully. The environmental cause of missing `rg` remains unknown, + but no remaining task depends on it. ### C7 — Inline locale maintenance could not obtain platform safety status @@ -191,6 +194,9 @@ The historical foundation evidence above is not evidence for this new candidate. for this agent's commits. Do not impersonate the owner or change global configuration. - Blocked dependants: commit attribution only. Implementation and checks continued. Resolution will be confirmed by the first successful attributed commit; next diagnostic is `git show` author/HEAD metadata. +- Resolution evidence: command-scoped identity produced commit + `425b0a8ea64c992ed6d2254b56f4e715260d9249` with author `ChatGPT `. + No global Git setting was changed. ### C3 — Edit invocation had indeterminate platform safety status @@ -291,3 +297,45 @@ The historical foundation evidence above is not evidence for this new candidate. - Added fixture test cases remain unexecuted because Swift is absent. `make test` and `make check` are attempted but blocked as C5/C8 describe. Portable catalog coverage, documentation links/index, and whitespace checks pass. - No live CAAM, SSH account operation, Netdata collection, credential inspection, installation, or deployment ran. + +### Presentation checkpoint and final sequential self-review + +The second checkpoint connects the typed core to Codex preferences, preserves the existing account-selection +flow, and supplies Overview, Accounts, Systems, and Work views. English catalogs add 73 new keys in each of +23 locales as explicitly labeled fallbacks, not completed translations. The portable checker covers 111 literal +keys across the affected preference/control/hub files, including multiline calls. + +| Finding | Disposition | +| --- | --- | +| A failed mutation could otherwise be mistaken for retryable failure | Closed effect classification; execute-once confirmation; same-UUID lookup; terminal fresh snapshot required | +| Refresh with an empty journal could erase a locally unknown operation | Preserve intent through refresh/restart; receipt bound to exact configuration; original-target lookup retained | +| Concurrent operations could overwrite receipt images | One application coordinator and global sequential operation gate; persistence precedes launch; no-effect storage failure cannot dispatch | +| Qualification keyed only by ID could survive endpoint replacement | Qualification and receipts bind full configuration; late-generation observations rejected | +| Cached provider display helpers can inspect credential-adjacent files | Hub consumes only already-published memory snapshots; historical costs are not attributed to CAAM accounts | +| Unlike spend/usage/host measurements could be blended or fabricated | Separate typed ledgers; explicit provenance, confidence, time, currency/window; no combined total; host metrics unsupported and nil | +| Default view construction could reach actual CAAM during tests | Existing test-process detection selects no-launch runner and memory-only receipts; explicit test clients are synthetic | +| Interrupted locale maintenance could leave non-English catalogs incomplete | Sync reconciles each catalog independently and preserves existing values; regression test proves restart idempotence | +| New source lines exceeded the repository's width guideline | Manually wrapped identified new-source lines; native SwiftFormat/SwiftLint remain unavailable, not marked passed | + +These are this agent's sequential correctness, security, maintainability, and performance **self-review**, +not independent review. Performance review establishes explicit bounds and asynchronous ownership, not measured +latency. No additional behavioral blocker was found by static inspection; native compilation, tests, and rendered +UI evidence remain required before production qualification or a merge decision. + +Actual checks after the presentation pass: + +- `python3 Scripts/test_check_caam_observability_locales.py`: exit 0; **5 tests passed**. These test only the + portable catalog helper (multiline/escaped literals, placeholders, bounds, restart-safe sync), not Swift behavior. +- `python3 Scripts/check_caam_observability_locales.py`: exit 0; **111 literal keys / 23 catalogs**, placeholders match. +- `node Scripts/check-documentation-links.mjs`: exit 0; **192 local links** checked. +- `node Scripts/generate-llms.mjs --check`: exit 0; documentation index current. +- Both unstaged and staged `git diff --check`: exit 0. +- New Swift tests: **29 declared cases** (24 Linux core cases and five macOS coordinator cases), plus fixed-clock + updates to existing projection/settings tests. They were **not executed** locally; C5/C8 describe exact blockers. +- No screenshot, interactive UI run, native build, native formatter pass, live gateway conformance, Netdata + telemetry, independent review, installation, or account switch is claimed. + +Publication remains one normal push of only `work/caam-codexbar-observability-hub` and one draft PR to the +Jones Systems fork. Production mutation qualification intentionally remains empty; CAAM dependency access, +native platform evidence, translation review, and Netdata API qualification are recorded limitations, not +reasons to withhold the independent implementation from a draft PR. From 76616f4b8cfea3aa65fc23c60a8992e0b5af365e Mon Sep 17 00:00:00 2001 From: ChatGPT Date: Sun, 6 Sep 2026 02:55:04 +0200 Subject: [PATCH 3/9] Resolve native CI lint findings and record draft delivery --- .../Observability/ObservabilityHub.swift | 26 ++++++++++---- .../Codex/CAAM/CAAMControlSession.swift | 20 +++++++---- .../Codex/CAAM/CAAMEnvironmentClient.swift | 5 ++- .../CAAM/CAAMEnvironmentValidation.swift | 3 +- .../CAAMEnvironmentCoordinatorTests.swift | 36 +++++++++++++------ .../CAAMEnvironmentSettingsTests.swift | 15 ++++++-- docs/work-notes/caam-environment-control.md | 32 +++++++++++++++++ 7 files changed, 108 insertions(+), 29 deletions(-) diff --git a/Sources/CodexBarCore/Observability/ObservabilityHub.swift b/Sources/CodexBarCore/Observability/ObservabilityHub.swift index 99b2df8bd6..7c8cdbb90c 100644 --- a/Sources/CodexBarCore/Observability/ObservabilityHub.swift +++ b/Sources/CodexBarCore/Observability/ObservabilityHub.swift @@ -181,7 +181,8 @@ public enum ObservabilityHub { historyDays: snapshot.historyDays, provenance: snapshot.costProvenance, evidence: .cached( - source: "Existing provider cost cache", observedAt: snapshot.updatedAt, + source: "Existing provider cost cache", + observedAt: snapshot.updatedAt, failed: input.costFailed, partial: !snapshot.historyCoverageIsEstablished || snapshot.costProvenance == .unknown, now: now)) @@ -195,7 +196,10 @@ public enum ObservabilityHub { confidence: input.usage?.dataConfidence ?? .unknown, evidence: .cached( source: input.source.isEmpty ? "Existing provider snapshot" : input.source, - observedAt: input.usage?.updatedAt, failed: input.usageFailed, partial: percent == nil, now: now), + observedAt: input.usage?.updatedAt, + failed: input.usageFailed, + partial: percent == nil, + now: now), cost: cost) } var work: [HubWorkObservation] = [] @@ -208,20 +212,28 @@ public enum ObservabilityHub { if cost.sessions.count > available { workIsPartial = true } for (index, session) in cost.sessions.prefix(available).enumerated() { work.append(HubWorkObservation( - id: "\(input.id):\(index)", provider: input.label, ordinal: index + 1, + id: "\(input.id):\(index)", + provider: input.label, + ordinal: index + 1, lastActivity: session.lastActivity, tokens: session.totalTokens.flatMap { $0 >= 0 ? $0 : nil }, estimatedCostUSD: self.nonnegative(session.costUSD), evidence: .cached( - source: "Recorded local session cache", observedAt: cost.updatedAt, - failed: input.costFailed, partial: true, now: now))) + source: "Recorded local session cache", + observedAt: cost.updatedAt, + failed: input.costFailed, + partial: true, + now: now))) } } } return ObservabilityHubSnapshot( - services: services, accounts: accounts, providers: providerRows, + services: services, + accounts: accounts, + providers: providerRows, hosts: services.map { NetdataReadOnlyFacade.unavailable(environmentID: $0.id) }, - work: work, workIsPartial: workIsPartial) + work: work, + workIsPartial: workIsPartial) } private static func nonnegative(_ value: Double?) -> Double? { diff --git a/Sources/CodexBarCore/Providers/Codex/CAAM/CAAMControlSession.swift b/Sources/CodexBarCore/Providers/Codex/CAAM/CAAMControlSession.swift index 246f7206a7..0060646acc 100644 --- a/Sources/CodexBarCore/Providers/Codex/CAAM/CAAMControlSession.swift +++ b/Sources/CodexBarCore/Providers/Codex/CAAM/CAAMControlSession.swift @@ -121,7 +121,8 @@ public struct CAAMControlSession: Sendable, Equatable { public mutating func beginPlan(target: String, fallback: Bool, now: Date) throws -> String { guard self.canForget else { throw CAAMControlFailure(reason: .busy) } let row = self.row(now: now) - guard let snapshot, (fallback ? row.canRestoreFallback : row.canSwitch), + let permitted = fallback ? row.canRestoreFallback : row.canSwitch + guard let snapshot, permitted, !fallback || target == snapshot.fallbackProfile, snapshot.profiles.contains(where: { $0.name == target && $0.eligible && !$0.active }) else { throw CAAMControlFailure(reason: .stale) } @@ -189,8 +190,8 @@ public struct CAAMControlSession: Sendable, Equatable { environmentID: self.configuration.id, operationID: operationID, expectedRevision: snapshot.revision) case .switchProfile, .restoreFallback: let row = self.row(now: now) - guard self.pendingOperationID == nil, let plan = confirmation.plan, - (confirmation.kind == .restoreFallback ? row.canRestoreFallback : row.canSwitch) + let permitted = confirmation.kind == .restoreFallback ? row.canRestoreFallback : row.canSwitch + guard self.pendingOperationID == nil, let plan = confirmation.plan, permitted else { throw CAAMControlFailure(reason: .stale) } try CAAMEnvironmentContract.validatePlan(plan, snapshot: snapshot, target: plan.targetProfile, now: now) let operationID = UUID() @@ -200,12 +201,17 @@ public struct CAAMControlSession: Sendable, Equatable { throw CAAMControlFailure(reason: .stale) } operation = .restoreFallback( - environmentID: self.configuration.id, expectedRevision: snapshot.revision, - planDigest: plan.planDigest, operationID: operationID, idempotencyKey: idempotencyKey) + environmentID: self.configuration.id, + expectedRevision: snapshot.revision, + planDigest: plan.planDigest, + operationID: operationID, + idempotencyKey: idempotencyKey) } else { operation = .executeSwitch( - environmentID: self.configuration.id, planDigest: plan.planDigest, - operationID: operationID, idempotencyKey: idempotencyKey) + environmentID: self.configuration.id, + planDigest: plan.planDigest, + operationID: operationID, + idempotencyKey: idempotencyKey) } self.expectedCommittedProfile = plan.targetProfile } diff --git a/Sources/CodexBarCore/Providers/Codex/CAAM/CAAMEnvironmentClient.swift b/Sources/CodexBarCore/Providers/Codex/CAAM/CAAMEnvironmentClient.swift index cefb8e5071..1adbafcf81 100644 --- a/Sources/CodexBarCore/Providers/Codex/CAAM/CAAMEnvironmentClient.swift +++ b/Sources/CodexBarCore/Providers/Codex/CAAM/CAAMEnvironmentClient.swift @@ -252,7 +252,10 @@ public struct CAAMEnvironmentClient: Sendable { guard let operationID = operation.operationID else { throw CAAMControlFailure(reason: .invalidResponse) } - let result = try await self.request(CAAMOperationResult.self, configuration: configuration, operation: operation) + let result = try await self.request( + CAAMOperationResult.self, + configuration: configuration, + operation: operation) do { try CAAMEnvironmentContract.validateOperationResult(result, operationID: operationID) } catch { diff --git a/Sources/CodexBarCore/Providers/Codex/CAAM/CAAMEnvironmentValidation.swift b/Sources/CodexBarCore/Providers/Codex/CAAM/CAAMEnvironmentValidation.swift index 1feecd70db..ee3381f9a5 100644 --- a/Sources/CodexBarCore/Providers/Codex/CAAM/CAAMEnvironmentValidation.swift +++ b/Sources/CodexBarCore/Providers/Codex/CAAM/CAAMEnvironmentValidation.swift @@ -310,7 +310,8 @@ public enum CAAMEnvironmentContract { if let object = value as? [String: Any] { return object.contains { key, child in let normalizedKey = key.lowercased().filter { $0.isASCII && ($0.isLetter || $0.isNumber) } - return forbiddenKeys.contains(normalizedKey) || self.containsForbiddenCredentialKey(child, depth: depth + 1) + return forbiddenKeys.contains(normalizedKey) || + self.containsForbiddenCredentialKey(child, depth: depth + 1) } } if let array = value as? [Any] { diff --git a/Tests/CodexBarTests/CAAMEnvironmentCoordinatorTests.swift b/Tests/CodexBarTests/CAAMEnvironmentCoordinatorTests.swift index 6dcf451423..d45e6cc93c 100644 --- a/Tests/CodexBarTests/CAAMEnvironmentCoordinatorTests.swift +++ b/Tests/CodexBarTests/CAAMEnvironmentCoordinatorTests.swift @@ -119,9 +119,13 @@ private enum CAAMCoordinatorFixture { static func snapshot(active: String = "primary") -> CAAMEnvironmentSnapshot { CAAMEnvironmentSnapshot( - revision: "1", observedAt: Self.now, caamVersion: "fixture", - capabilities: CAAMControlCapability.allCases.map(\.rawValue), reachability: .reachable, - hostDefaultProfile: active, fallbackProfile: "secondary", + revision: "1", + observedAt: Self.now, + caamVersion: "fixture", + capabilities: CAAMControlCapability.allCases.map(\.rawValue), + reachability: .reachable, + hostDefaultProfile: active, + fallbackProfile: "secondary", profiles: ["primary", "secondary"].map { name in CAAMEnvironmentProfile( name: name, active: name == active, system: false, eligible: true, health: .healthy) @@ -133,9 +137,14 @@ private enum CAAMCoordinatorFixture { let encoder = JSONEncoder() encoder.dateEncodingStrategy = .iso8601 let data = try encoder.encode(CAAMControlEnvelope( - schema: CAAMEnvironmentContract.schema, kind: kind, environmentID: Self.configuration.id, - protocolVersion: "1.0", result: value, error: nil)) - return SubprocessResult(stdout: String(decoding: data, as: UTF8.self), stderr: "") + schema: CAAMEnvironmentContract.schema, + kind: kind, + environmentID: Self.configuration.id, + protocolVersion: "1.0", + result: value, + error: nil)) + let stdout = try #require(String(data: data, encoding: .utf8)) + return SubprocessResult(stdout: stdout, stderr: "") } } @@ -150,8 +159,12 @@ private actor CAAMCoordinatorFixtureRunner: CAAMEnvironmentCommandRunning { return try CAAMCoordinatorFixture.response(CAAMCoordinatorFixture.snapshot(), kind: kind) case "plan-switch": return try CAAMCoordinatorFixture.response(CAAMSwitchPlan( - planDigest: String(repeating: "a", count: 64), expectedRevision: "1", currentProfile: "primary", - targetProfile: "secondary", affectedProfiles: ["primary", "secondary"], reloadRequired: false, + planDigest: String(repeating: "a", count: 64), + expectedRevision: "1", + currentProfile: "primary", + targetProfile: "secondary", + affectedProfiles: ["primary", "secondary"], + reloadRequired: false, expiresAt: CAAMCoordinatorFixture.now.addingTimeInterval(30)), kind: kind) case "execute-switch": throw SubprocessRunnerError.timedOut("fixture interruption") @@ -160,8 +173,11 @@ private actor CAAMCoordinatorFixtureRunner: CAAMEnvironmentCommandRunning { throw SubprocessRunnerError.launchFailed("fixture invalid operation ID") } return try CAAMCoordinatorFixture.response(CAAMOperationResult( - operationID: operationID, state: .committed, effect: .knownEffect, - previousProfile: "primary", currentProfile: "secondary", + operationID: operationID, + state: .committed, + effect: .knownEffect, + previousProfile: "primary", + currentProfile: "secondary", snapshot: CAAMCoordinatorFixture.snapshot(active: "secondary")), kind: kind) default: throw SubprocessRunnerError.launchFailed("fixture unexpected operation") diff --git a/Tests/CodexBarTests/CAAMEnvironmentSettingsTests.swift b/Tests/CodexBarTests/CAAMEnvironmentSettingsTests.swift index 47e8317069..a3e4c3dd1e 100644 --- a/Tests/CodexBarTests/CAAMEnvironmentSettingsTests.swift +++ b/Tests/CodexBarTests/CAAMEnvironmentSettingsTests.swift @@ -11,7 +11,10 @@ struct CAAMEnvironmentSettingsTests { let settings = Self.makeSettingsStore(suite: suite) defer { Self.clearSettingsStore(settings, suite: suite) } let store = Self.makeUsageStore(settings: settings) - let pane = ProvidersPane(settings: settings, store: store, caamEnvironmentCoordinator: CAAMEnvironmentCoordinator()) + let pane = ProvidersPane( + settings: settings, + store: store, + caamEnvironmentCoordinator: CAAMEnvironmentCoordinator()) let state = pane._test_codexEnvironmentsSectionState() @@ -26,7 +29,10 @@ struct CAAMEnvironmentSettingsTests { let settings = Self.makeSettingsStore(suite: suite) defer { Self.clearSettingsStore(settings, suite: suite) } let store = Self.makeUsageStore(settings: settings) - let pane = ProvidersPane(settings: settings, store: store, caamEnvironmentCoordinator: CAAMEnvironmentCoordinator()) + let pane = ProvidersPane( + settings: settings, + store: store, + caamEnvironmentCoordinator: CAAMEnvironmentCoordinator()) let configurations = [ CAAMEnvironmentConfiguration(id: "vps", label: "VPS", connection: .ssh(destination: "vps")), CAAMEnvironmentConfiguration(id: "laptop", label: "Laptop", connection: .local()), @@ -49,7 +55,10 @@ struct CAAMEnvironmentSettingsTests { let settings = Self.makeSettingsStore(suite: suite) defer { Self.clearSettingsStore(settings, suite: suite) } let store = Self.makeUsageStore(settings: settings) - let pane = ProvidersPane(settings: settings, store: store, caamEnvironmentCoordinator: CAAMEnvironmentCoordinator()) + let pane = ProvidersPane( + settings: settings, + store: store, + caamEnvironmentCoordinator: CAAMEnvironmentCoordinator()) let unsafe = [ CAAMEnvironmentConfiguration( id: "vps", diff --git a/docs/work-notes/caam-environment-control.md b/docs/work-notes/caam-environment-control.md index 3e9027ff26..2b0b7215f1 100644 --- a/docs/work-notes/caam-environment-control.md +++ b/docs/work-notes/caam-environment-control.md @@ -108,6 +108,18 @@ The historical foundation evidence above is not evidence for this new candidate. ## Connector and runtime incidents +### C10 — Bounded CI log preview closed its pipe early + +- Intended effect/action: read failed lint diagnostics for this draft PR using `gh api` on its completed job log, + filter output, and cap the preview with `head`. +- Native result: exit 141 with pipefail enabled; 3437 output tokens. The filter included progress lines, + so the preview filled before the actual diagnostics. This is a partial read/SIGPIPE, not a failed write. +- Postcondition/workaround: narrow the filter to errors/warnings/summary and use `sed -n` to drain the stream + while bounding displayed lines. Retry exited 0 (2624 tokens) and supplied all 40 reported lint violations. +- Blocked dependants: first diagnostic preview only. No repository, branch, PR, or credential mutation occurred. + Remediation of the actual source findings continued. Resolution: resolved; next diagnostic on recurrence + is pipeline exit/status interpretation, not retrying a publication operation. + ### C8 — Standard check entrypoint cannot launch Apple's catalog parser - Intended effect/action: run the required repository entrypoint `make check`. @@ -339,3 +351,23 @@ Publication remains one normal push of only `work/caam-codexbar-observability-hu Jones Systems fork. Production mutation qualification intentionally remains empty; CAAM dependency access, native platform evidence, translation review, and Netdata API qualification are recorded limitations, not reasons to withhold the independent implementation from a draft PR. + +### Published draft and CI remediation + +- Normal non-force push of the task branch succeeded against the verified fork push destination. + Prior matching PR search returned none. Created **draft PR #2**: + . API readback confirmed OPEN, draft=true, + base `main`, exact task head, and implementation HEAD `db5e39a36be393724d6efbd85addc5085054a098`. +- Implementation commits: `425b0a8ea64c992ed6d2254b56f4e715260d9249` (typed core/models/fixtures) + and `db5e39a36be393724d6efbd85addc5085054a098` (app integration, UI, catalog helper/tests, specification). +- Re-fetched canonical main before publication; it remained the original `d6e929cd736eccae187cd41ddb5305a670afb2c8`. + Worktree was clean; the complete implementation diff covered 51 files, including 23 append-only locale catalogs. +- Existing CI run started for that HEAD. + Path-gate detection passed; macOS native tests were explicitly skipped by draft policy. Linux x64/ARM64 + builds/tests and musl build were in progress at first inspection, not passed. +- CI lint supplied real native evidence unavailable locally: **40 serious violations across 2116 files**, all + in the touched CAAM/hub sources and fixtures. Findings were unnecessary guard parentheses, mixed multiline + arguments, missed 120-column limits, and one non-failable fixture Data-to-String conversion. +- Remediation preserves behavior: use named permission booleans, one argument per line for mixed-layout calls, + wrap long touched lines, and require valid fixture UTF-8. The bounded diagnostic retrieval issue is C10 above. + The same draft PR receives the remediation commit and normal push; CI will rerun for its exact HEAD. From 443071c8c704b1102ce27f417dcef81e0d9fe912 Mon Sep 17 00:00:00 2001 From: ChatGPT Date: Sun, 6 Sep 2026 03:02:38 +0200 Subject: [PATCH 4/9] Record final draft publication and verification evidence --- docs/work-notes/caam-environment-control.md | 41 +++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/docs/work-notes/caam-environment-control.md b/docs/work-notes/caam-environment-control.md index 2b0b7215f1..b9f1fd3cdb 100644 --- a/docs/work-notes/caam-environment-control.md +++ b/docs/work-notes/caam-environment-control.md @@ -108,6 +108,28 @@ The historical foundation evidence above is not evidence for this new candidate. ## Connector and runtime incidents +### C11 — GitHub CLI body edit failed on a deprecated GraphQL field + +- Intended effect/action: update only draft PR #2's description with the final implementation/check/incident summary. +- Safe invocation shape: `gh pr edit 2 --repo Jones-Systems/CodexBar --body `. + No token, environment, recovery-bundle, or permission inspection was requested. +- Native result: exit 1, 52 output tokens; GraphQL rejected `repository.pullRequest.projectCards`, with + a message about deprecated Projects (classic). This is a CLI/API-schema failure, not evidence of denied + repository ownership or publication authorization. +- Effect reconciliation: an initial grouped read-only REST/CI-status check was platform-blocked because safety + status could not be determined; it returned no subprocess exit/session. A simple `gh pr view ... --json` + then exited 0 (966 tokens), confirming the original PR body was unchanged, draft=true, and head at + `76616f4b8cfea3aa65fc23c60a8992e0b5af365e`. The attempted body edit had no observed effect. +- Workaround: prepare the bounded description in ignored repository scratch space and use GitHub's REST + pull-request update through the same bound connector and already-qualified host credential. +- Blocked dependants: description refresh only; branch push and draft creation already succeeded. No new PR, + force push, credential change, or host installation is needed. Resolution pending the REST postcondition; + next diagnostic is the updated PR body/head/draft readback. +- Resolution evidence: REST `PATCH repos/Jones-Systems/CodexBar/pulls/2` with a repository-local body file + exited 0 and returned the existing PR URL (13 output tokens). Subsequent PR readback exited 0 (1740 tokens), + showed the updated description and C1-C11 reference, and preserved OPEN/draft=true and the exact task head. + No additional PR or repository setting was created or changed. + ### C10 — Bounded CI log preview closed its pipe early - Intended effect/action: read failed lint diagnostics for this draft PR using `gh api` on its completed job log, @@ -371,3 +393,22 @@ reasons to withhold the independent implementation from a draft PR. - Remediation preserves behavior: use named permission booleans, one argument per line for mixed-layout calls, wrap long touched lines, and require valid fixture UTF-8. The bounded diagnostic retrieval issue is C10 above. The same draft PR receives the remediation commit and normal push; CI will rerun for its exact HEAD. + +### Final delivery verification + +- Remediation commit `76616f4b8cfea3aa65fc23c60a8992e0b5af365e` was normally pushed and verified as + the head of draft PR #2. The replacement CI run is + . +- **Native CI lint passed** on that implementation HEAD: completed successfully at + `2026-09-06T00:57:03Z`, job + . + The earlier 40 findings are therefore remediated with successful rerun evidence, not merely source edits. + This is the existing Linux lint route; it does not establish a macOS SwiftFormat or UI-rendering result. +- Path-gate detection also passed. Linux x64/ARM64 build/test jobs and the musl build remained in progress at + this inspection. macOS native tests were explicitly skipped for the draft. No unobserved build/test pass is claimed. +- Final metadata-only checkpoint adds C11 and these verified delivery outcomes to this same Work Note; + it changes no implementation, tests, catalog, contract, or workflow. PR description update used the + already-qualified REST route after the GraphQL failure, with postcondition readback confirming one open draft. +- Persistent incident evidence is this committed path, `docs/work-notes/caam-environment-control.md`, + linked from . All incidents C1-C11 include bounded + native evidence, effect classification, postconditions, workarounds, and remaining diagnostics. From 4885a8414396889ed799c4a35263154ceb5aa5a2 Mon Sep 17 00:00:00 2001 From: ChatGPT Date: Sun, 6 Sep 2026 03:18:16 +0200 Subject: [PATCH 5/9] Repair CAAM fixture compilation and preserve native failure evidence --- .../CAAMControlTransportLinuxTests.swift | 4 +- docs/work-notes/caam-environment-control.md | 47 +++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/TestsLinux/CAAMControlTransportLinuxTests.swift b/TestsLinux/CAAMControlTransportLinuxTests.swift index a4f7337eef..5a9f0f569d 100644 --- a/TestsLinux/CAAMControlTransportLinuxTests.swift +++ b/TestsLinux/CAAMControlTransportLinuxTests.swift @@ -5,7 +5,9 @@ import Testing struct CAAMControlTransportLinuxTests { @Test func `relative PATH entries cannot choose a gateway from the working directory`() { - #expect(CAAMEnvironmentClient.resolveLocalGateway(environment: ["PATH": ".:relative/bin"]) == nil) + #expect(CAAMEnvironmentClient.resolveLocalGateway( + configuredPath: nil, + environment: ["PATH": ".:relative/bin"]) == nil) } private func client(_ runner: CAAMControlFixtureRunner) -> CAAMEnvironmentClient { diff --git a/docs/work-notes/caam-environment-control.md b/docs/work-notes/caam-environment-control.md index b9f1fd3cdb..4a2bd9aff0 100644 --- a/docs/work-notes/caam-environment-control.md +++ b/docs/work-notes/caam-environment-control.md @@ -108,6 +108,48 @@ The historical foundation evidence above is not evidence for this new candidate. ## Connector and runtime incidents +### C14 — Linux fixture compilation omitted a required argument + +- Observation: audit read at `2026-09-06T01:15:35Z`; CI emitted the same diagnostic on ARM64 at + `2026-09-06T01:12:19.5032651Z` and x64 at `2026-09-06T01:13:39.7199048Z`. + Phase/intended effect: native verification of the published candidate, not a live account operation. +- Action/shape: existing CI `swift test --parallel`; diagnosis through connector `exec_command` + with `gh api repos/Jones-Systems/CodexBar/actions/jobs//logs`, filtered and fully drained + with `sed` while limiting the displayed excerpt. No raw log was persisted. +- Native result: both Linux jobs COMPLETED/FAILURE; test-step process exit 1. Exact useful excerpt: + `TestsLinux/CAAMControlTransportLinuxTests.swift:8:59: error: missing argument for parameter 'configuredPath' in call`. + Run `34002864831`, x64 job `101404777530`, ARM64 job `101404777527`, head + `443071c8c704b1102ce27f417dcef81e0d9fe912`. Log reads exited 0 (1201 and 414 rendered output tokens). + Original stdout/stderr byte counts and compiler exit separate from the test step are unavailable. +- Effect classification: **partial** verification. Release compilation succeeded; tests could not compile, + CLI smoke steps were skipped, and the aggregate gate failed. No live account or repository write by the failed tests. +- Reconciliation/repair: inspected the actual method signature, which requires `configuredPath: String?`; + add explicit `configuredPath: nil` in the relative-PATH fixture. Production code is unchanged. +- Blocked dependants: native fixture execution and green Linux/aggregate checks. Unaffected work: + lint and musl build passed; diagnostic audit, portable checks, commits, and draft publication continue. +- Disposition: source repair applied; native rerun pending. Next diagnostic: replacement CI test-step + result on the normally pushed repair commit. Local Swift remains unavailable (C5). + +### C13 — Diagnostic helper patch received indeterminate platform safety status + +- Observation: audit pass on 2026-09-06 UTC, after the workspace/PR read at 01:15:35Z; + exact rejection timestamp was not supplied. Phase: incident audit and CI failure diagnosis. +- Intended effect/action: connector `apply_patch` to add an ignored, repository-local bounded CI-log reader + at `.build/caam-diagnostic-read.py`. The requested helper would drain a public project job log, + retain only bounded sanitized diagnostics, and report stream byte counts and digests. +- Native result: "This tool call was blocked by OpenAI because we couldn't determine the safety status + of the request." No native classification/code beyond that message, process exit, stream sizes, + or command session were supplied. The original payload is not persisted. +- Effect: initially unknown; reconciled to **known-no-effect**. Connector `read` of `.build` returned + exactly one existing entry, `caam-observability-pr-body.md`; the proposed helper did not exist. +- Attempts/alternative: one patch attempt; no blind replay. Use the existing `gh api` read route for + bounded job metadata/annotations instead. The x64 job metadata read exited 0 (581 output tokens), + confirming release build success and failure specifically in the Swift-test step. +- Blocked dependants: only the proposed stream-count helper. Work Note editing, Git access, public + documentation research, CI metadata inspection, and publication remain available. +- Disposition: helper path abandoned after no-effect readback; runtime safety cause remains unknown. + Next diagnostic: read the failing job's bounded annotations; no tooling install or permission changes. + ### C11 — GitHub CLI body edit failed on a deprecated GraphQL field - Intended effect/action: update only draft PR #2's description with the final implementation/check/incident summary. @@ -156,6 +198,8 @@ The historical foundation evidence above is not evidence for this new candidate. fixture authoring, portable validation, commits, and draft publication continue. - Resolution: platform dependency remains absent; next diagnostic is existing CI against the candidate, not tool installation on the bound host. +- Audit rerun after the C14 fixture repair: `make check` again exited 2; stdout 23 bytes, + stderr 1288 bytes. Minimal excerpt: `Error: spawnSync plutil ENOENT`. No raw log was saved. ### C9 — A later inspection process could no longer resolve ripgrep @@ -217,6 +261,9 @@ The historical foundation evidence above is not evidence for this new candidate. portable checks, documentation, and publication continue. Existing Linux CI may provide native evidence; existing macOS CI policy explicitly defers macOS tests on draft PRs. - Resolution: toolchain limitation remains. Next diagnostic: native CI outcomes for the exact published HEAD. +- Audit rerun after the C14 fixture repair: `make test` again exited 2; stdout 384 bytes, + stderr 1702 bytes. Minimal excerpt: `FileNotFoundError: [Errno 2] No such file or directory: 'swift'`. + No raw traceback was saved; zero native test execution remains the result. ### C6 — Repository Git author identity is unset From 0bdf8a262bc9342d9b62a5f12f8062db22273b50 Mon Sep 17 00:00:00 2001 From: ChatGPT Date: Sun, 6 Sep 2026 03:33:16 +0200 Subject: [PATCH 6/9] Complete bounded connector and runtime incident audit --- docs/work-notes/caam-environment-control.md | 294 +++++++++++++++++++- 1 file changed, 282 insertions(+), 12 deletions(-) diff --git a/docs/work-notes/caam-environment-control.md b/docs/work-notes/caam-environment-control.md index 4a2bd9aff0..411d4297dd 100644 --- a/docs/work-notes/caam-environment-control.md +++ b/docs/work-notes/caam-environment-control.md @@ -60,7 +60,7 @@ Synthesis: 5. The first PR implements the safe read-only/configuration foundation and capability-gates mutations. Later CAAM delivery activates mutation without redesigning the UI or transport. 6. A dedicated Environments section inside the existing Codex provider settings is the smallest reversible first placement. A top-level settings destination remains a later product decision after real macOS rendering evidence. -The coordinator is the sole Git owner of `/home/malcolmjones/Projects/CodexBar-worktrees/caam-environments` on `work/caam-environments`. No other writer is assigned. The owner selected `Jones-Systems/CodexBar` as the organization-controlled delivery fork; no pull request will target the upstream `steipete/CodexBar` repository. +The historical coordinator was the sole Git owner of the foundation task worktree (private absolute path omitted) on `work/caam-environments`. No other writer was assigned. The owner selected `Jones-Systems/CodexBar` as the organization-controlled delivery fork; no pull request targets the upstream `steipete/CodexBar` repository. The combined project's current branch is recorded below. @@ -108,6 +108,136 @@ The historical foundation evidence above is not evidence for this new candidate. ## Connector and runtime incidents +### Audit scope and evidence conventions + +This is the single evolving diagnostic record for the combined project, on the existing task branch. +Audit began with native workspace/PR readback at `2026-09-06T01:15:35Z`: clean worktree, same process/root/Git +owner, no merge/rebase/cherry-pick/revert state, HEAD `443071c8c704b1102ce27f417dcef81e0d9fe912`, and one +OPEN draft PR #2 against `Jones-Systems/CodexBar:main`. No diagnostic branch or linked worktree was created. +Times in this section are UTC. Historical events with no returned timestamp are labeled **not supplied**; +the audit date is not substituted for their occurrence time. No missing native telemetry is reconstructed. + +Evidence precedence: returned native tool/process/check results; verified repository/PR postconditions; +then the contemporaneous Work Note where the original rejection is no longer present in the available +tool history. C1-C3, C7's rejected calls, and C11's blocked reconciliation preserve that latter evidence +without pretending to have retrieved a missing native envelope. Public documentation supplies context +only, never proof that a private invocation ran or that an operation had an effect. + +**Output convention for every entry:** `original_token_count` is the connector's reported rendered-output +token count, not stdout/stderr bytes and not a truncation flag. Original stream separation, byte sizes, +HTTP status, diagnostic classification/code, process exit, and truncation metadata are **unavailable unless +explicitly stated**. A platform block without process telemetry must not be called an exit-0 command. +Only small sanitized excerpts are kept; raw logs, invocation bodies, workspace handles, host paths, +protected environment values, credentials, and private prompts/transcripts are excluded. +The final document has a 72 KiB publication bound; the earlier 64 KiB checks remain historical results. + +Pre-project foundation limitations (including the historical Mac gateway denial, missing `jq`, and +SwiftLint library failure) remain labeled historical above; no native records for those older events were +available to this combined project. They are not counted as newly observed incidents. Expected injected +negative-fixture output is not a host failure: the cleanup-timeout diagnostic is produced by +`Scripts/test_swift_test_process_cleanup.py:1003-1025`, which mocks `Popen`, signals, time, and waiting. + +**Disposition convention:** resolved includes a verified authorized workaround; it does not assert that +the platform or missing-utility root cause was repaired. Open means a dependency or required verification +remains unavailable. Effects are `known-no-effect`, `desired-effect-observed`, `partial`, or `unknown`; +original attempts and successful alternatives are distinguished. No possibly effective repository write +is currently unresolved. Missing CAAM evidence is not evidence of a CAAM mutation. + +| ID | Incident family | Disposition at audit | +| --- | --- | --- | +| C1 | Virtual/native command-path mismatch | Resolved | +| C2 | Platform-blocked external dependency inspection | Open: CAAM evidence unavailable | +| C3 | Indeterminate inline source-edit request | Resolved by verified patch | +| C4 | Nonexistent inspection globs / out-of-range read | Resolved by exact source reads | +| C5 | Local Swift discovery unavailable | Open: local native checks unavailable | +| C6 | Git author identity absent | Resolved with command-scoped attribution | +| C7 | Indeterminate locale-maintenance requests | Resolved by bounded repository helper | +| C8 | Local native catalog parser unavailable | Open: standard native check unavailable | +| C9 | Ripgrep disappeared from later process PATH | Resolved by available inspection tools; cause unknown | +| C10 | Preview pipeline SIGPIPE / incomplete diagnostic selection | Resolved by drained bounded filter | +| C11 | Deprecated GraphQL field and blocked reconciliation attempt | Resolved by readback and REST update | +| C12 | Native CI lint violations | Resolved with passing native rerun | +| C13 | Indeterminate diagnostic-helper patch | Resolved with no-effect readback and existing read tools | +| C14 | Required fixture argument omitted | Resolved: ARM64 and x64 native test reruns passed | +| C15 | Diagnostic completeness assertions lacked explicit fields | Resolved by field corrections and passing rerun | +| C16 | Aggregate CI deliberately fails while macOS tests are deferred | Open verification gate; draft policy preserved | + +At this checkpoint: **16 incident families; 12 resolved, 4 open**. Recurrences/failed attempts are retained +within the original stable ID, not silently discarded or counted as independent root causes. +Highest-impact open incident is C2: pinned CAAM evidence is unavailable, blocking production conformance +qualification. C5/C8 block local native checks, not publication. C14's successful reruns are recorded below. +C16 separately leaves aggregate CI incomplete until required macOS evidence exists; it is an intentional +gate, not a connector defect, and does not block this authorized draft publication. + +### C16 — Aggregate verification remains incomplete under the required draft policy + +- Observation time: native gate output at `2026-09-06T01:29:35.5854745Z`; job completed `01:29:37Z`. + Phase/intended effect: verify all required CI jobs after the successful Linux fixture repair. +- Action/shape: CI `Scripts/ci_verify_test_jobs.sh success success true skipped true true success success`; + connector `exec_command` read run/job metadata and a bounded, drained log excerpt. The script was then + inspected through connector `read`; lines 36-38 deliberately reject required-but-deferred macOS tests. +- Native status/code/exit: run `34003546574` completed `failure`; job `101407868656`, step + `Verify required CI jobs`, completed `failure`, process exit **1**. Exact excerpt: + `macOS Swift tests are required but deferred; aggregate CI remains incomplete`. +- Output/evidence limits: successful log retrieval exited 0, 338 rendered tokens; original CI stdout/stderr + byte counts unavailable. No raw log or environment dump is persisted; only public job states are retained. +- Effect: **partial** verification, **known-no-effect** on repository/PR content. Lint, path detection, both + Linux native build/test/smoke jobs and musl all passed; macOS was skipped under draft policy. +- Attempts/reconciliation: inspect the completed failing gate rather than replaying successful builds or + weakening CI. Keep draft PR #2 as explicitly required. Musl job `101406596572` succeeded at `01:29:29Z`. +- Blocked dependants: aggregate-green verification and macOS native proof. Unaffected work: diagnostic audit, + fixture remediation, Linux native/portable verification, commit/push and draft-PR updates completed or continue. +- Disposition: **open, expected policy gate**. Next diagnostic: required macOS build/test evidence through + an authorized future verification path. Do not mark this PR ready, edit CI to skip the gate, merge, or install + a toolchain merely to turn the aggregate green. No successful macOS or complete aggregate result is claimed. +- Controlled local reproduction with those fixed job-state arguments: native script exit 1, stdout **0 bytes**, + stderr **77 bytes**, exactly the excerpt above plus its newline. The diagnostic wrapper exited 0 because + it verified the expected rejection; that does not change the aggregate failure to success. + +### C15 — Diagnostic completeness check required an explicit blocked-dependants field + +- Observation time: `2026-09-06T01:20:48Z`. Phase/intended effect: verify report completeness and public safety. +- Action/shape: connector `exec_command`, read-only Python assertions over incident headings/field families, + followed by read-only source/CI inspection and canonical fetch only if assertions succeeded. +- Native result: exit 1; `AssertionError: C9`. Preceding output confirmed 14 unique IDs, 57798 report bytes, + and no prohibited literals. `original_token_count=153`; separate stdout/stderr byte counts unavailable. +- Effect: **partial** validation, **known-no-effect** on files/Git. The grouped later inspections/fetch did + not execute because the shell used `set -eu`. C9 described dependencies but lacked the checked explicit label. +- Attempts/repair/postcondition: add C9's blocked-dependants field and record this failed check; no broad + mutation replay. Unaffected report content and the already-pushed fixture repair remain intact. +- Blocked dependants: completeness-check acceptance and the skipped grouped inspections only. Work Note + editing and publication remained available. Initial disposition: repaired, rerun pending. Next diagnostic: rerun + the read-only field audit, then complete the skipped checks. No raw traceback or private payload retained. +- Second audit attempt (same pass; exact second not supplied): collecting all missing labels returned exit 1, + 380 rendered tokens, with `missing fields: {'C5': ['phase']}`. Add the explicit phase label to C5; + no mutation was requested by the checker and the following grouped checks again did not execute. +- Resolution/postcondition: third audit attempt passed, 15 entries, 59810 bytes, no missing fields; the + command session completed exit 0. Skipped fetch and CI inspection then completed successfully: + canonical main remained `d6e929cd736eccae187cd41ddb5305a670afb2c8` and reachable. Disposition: + **resolved**, correction **desired-effect-observed**. Next diagnostic is the final post-edit audit. + +### C12 — Native CI lint rejected candidate source layout + +- Observation time: lint diagnostics `2026-09-06T00:51:05Z` through `00:51:22Z`; job completed + `00:51:24Z`. Phase/intended effect: verify candidate source with existing repository CI. +- Action / sanitized shape: CI `./Scripts/lint.sh lint-linux`; connector `exec_command` reads job metadata + and `gh api repos/Jones-Systems/CodexBar/actions/jobs/101403255475/logs` with bounded filtering. +- Native status/code/exit: job `completed` / `failure`; `Done linting! Found 40 violations, 40 serious in 2116 files.`; + process exit 2. Native rule codes: `control_statement`, `multiline_arguments`, `line_length`, + `optional_data_string_conversion`. No connector failure code was reported for the successful read. +- Output/evidence limits: successful diagnostic read exit 0, `original_token_count=2624`; separate original + stdout/stderr byte sizes unavailable. The preceding partial read is C10, not another source-lint failure. +- Effect: **partial** verification; no live-account effect. All reported violations were in touched files. + Remediation commit `76616f4b8cfea3aa65fc23c60a8992e0b5af365e` removed unnecessary guard parentheses, + separated multiline arguments, wrapped long lines, and required valid fixture UTF-8. +- Attempts/alternative/postcondition: normal push reran CI; job `101403882429` completed **success** at + `2026-09-06T00:57:03Z`. Later job `101404777472` also completed **SUCCESS** at `01:04:40Z` on + `443071c8c704b1102ce27f417dcef81e0d9fe912`. Desired remediation effect is observed. +- Blocked dependants: initial lint/aggregate approval only. Unaffected work: code review, fixture authoring, + documentation, branch publication, draft PR, and other CI jobs continued. +- Disposition: **resolved**. Next diagnostic: the next candidate's lint result; no local tool installation. + These Linux lint results do not establish macOS SwiftFormat, UI rendering, or native test execution. + ### C14 — Linux fixture compilation omitted a required argument - Observation: audit read at `2026-09-06T01:15:35Z`; CI emitted the same diagnostic on ARM64 at @@ -127,8 +257,18 @@ The historical foundation evidence above is not evidence for this new candidate. add explicit `configuredPath: nil` in the relative-PATH fixture. Production code is unchanged. - Blocked dependants: native fixture execution and green Linux/aggregate checks. Unaffected work: lint and musl build passed; diagnostic audit, portable checks, commits, and draft publication continue. -- Disposition: source repair applied; native rerun pending. Next diagnostic: replacement CI test-step - result on the normally pushed repair commit. Local Swift remains unavailable (C5). +- Initial disposition: source repair applied; native rerun pending. The next diagnostic was replacement + CI test-step evidence on the pushed repair commit; successful results follow. Local Swift remains unavailable (C5). +- Repair/publication postcondition: commit `4885a8414396889ed799c4a35263154ceb5aa5a2` normally pushed + to this same task branch (connector session completed exit 0; 40 rendered output tokens). + Portable helper tests (5), 111-key/23-catalog coverage, 192 documentation links, index and whitespace + checks passed again. This is source/transport proof, not a substitute for the pending native rerun. +- Resolution: repair run `34003546574`, HEAD `4885a8414396889ed799c4a35263154ceb5aa5a2`, passed the + ARM64 Swift-test step at `2026-09-06T01:27:33Z` (job `101406564355`) and x64 at `01:28:53Z` + (job `101406564418`); both complete jobs and CLI smoke steps reported **success**. Each log reported + 507 tests / 71 suites plus a separate 8-test / 1-suite run passing. Bounded log reads exited 0, 62 + rendered tokens each; original stream byte sizes unavailable. **Resolved; desired-effect-observed**. + Next diagnostic: normal candidate CI; macOS native evidence and local C5/C8 remain separate. ### C13 — Diagnostic helper patch received indeterminate platform safety status @@ -148,10 +288,23 @@ The historical foundation evidence above is not evidence for this new candidate. - Blocked dependants: only the proposed stream-count helper. Work Note editing, Git access, public documentation research, CI metadata inspection, and publication remain available. - Disposition: helper path abandoned after no-effect readback; runtime safety cause remains unknown. - Next diagnostic: read the failing job's bounded annotations; no tooling install or permission changes. + The next diagnostic at that point was the bounded job annotations; its successful outcome follows. + No tooling install or permission changes were needed. +- Alternative result: annotations read exited 0 (29 tokens) and reported test-step exit 1. Drained bounded + log reads exited 0, yielding the exact C14 compiler diagnostic for both architectures. Thus this diagnostic + lane is **resolved by workaround**, with **desired-effect-observed** for obtaining the needed evidence. ### C11 — GitHub CLI body edit failed on a deprecated GraphQL field +- Observation time: implementation-turn publication, exact invocation/rejection timestamps **not supplied**; + resolved before the `443071c8c` delivery checkpoint. Phase: draft PR description synchronization. +- Output limits: original stdout/stderr byte counts and HTTP status unavailable. The 52, 966, and 1740 + token counts below refer to separate connector renderings, not combined native stream sizes. +- Native classification: CLI GraphQL schema error; no standalone machine error code was returned. + C11-a is the exit-1 edit; C11-b is the separate indeterminate read-only reconciliation request; + C11-c/d are the successful readback/REST alternative. The two failed attempts are not conflated. +- Effect/disposition: original edit **unknown** until readback, then **known-no-effect** for the intended body + update; REST update **desired-effect-observed**. **Resolved**. No unknown publication effect remains. - Intended effect/action: update only draft PR #2's description with the final implementation/check/incident summary. - Safe invocation shape: `gh pr edit 2 --repo Jones-Systems/CodexBar --body `. No token, environment, recovery-bundle, or permission inspection was requested. @@ -164,16 +317,28 @@ The historical foundation evidence above is not evidence for this new candidate. `76616f4b8cfea3aa65fc23c60a8992e0b5af365e`. The attempted body edit had no observed effect. - Workaround: prepare the bounded description in ignored repository scratch space and use GitHub's REST pull-request update through the same bound connector and already-qualified host credential. -- Blocked dependants: description refresh only; branch push and draft creation already succeeded. No new PR, - force push, credential change, or host installation is needed. Resolution pending the REST postcondition; - next diagnostic is the updated PR body/head/draft readback. +- Blocked dependants: description refresh only; branch push and draft creation already succeeded. Unaffected + work: incident editing, local verification, and CI inspection. Next diagnostic on recurrence is a read-only + body/head/draft readback; neither a new PR nor credential or host changes are needed. - Resolution evidence: REST `PATCH repos/Jones-Systems/CodexBar/pulls/2` with a repository-local body file exited 0 and returned the existing PR URL (13 output tokens). Subsequent PR readback exited 0 (1740 tokens), showed the updated description and C1-C11 reference, and preserved OPEN/draft=true and the exact task head. No additional PR or repository setting was created or changed. +- Public context checked during this audit: GitHub documents the deprecated Projects (classic) GraphQL + fields and the REST pull-request update endpoint. These references support the repair route, not native + effect inference: and + . ### C10 — Bounded CI log preview closed its pipe early +- Observation time: publication-turn CI diagnosis after the failed `00:51:24Z` lint job and before commit + `76616f4b8`; exact command timestamp **not supplied**. Phase: diagnose native CI findings. +- Action/shape: connector `exec_command`, `gh api | | head `. + Native classification: pipeline exit **141**, interpreted as SIGPIPE; no independent connector code reported. +- Output limits: stdout/stderr byte sizes and individual pipeline-component exits unavailable; 3437 tokens + is rendered output, not proof of a complete log. Progress lines exhausted the selected preview. +- Effect: **partial** diagnostic read; **known-no-effect** on repository/PR. Alternative diagnostic read + **desired-effect-observed**. No raw log or credentials are retained. Disposition: **resolved**. - Intended effect/action: read failed lint diagnostics for this draft PR using `gh api` on its completed job log, filter output, and cap the preview with `head`. - Native result: exit 141 with pipefail enabled; 3437 output tokens. The filter included progress lines, @@ -183,9 +348,21 @@ The historical foundation evidence above is not evidence for this new candidate. - Blocked dependants: first diagnostic preview only. No repository, branch, PR, or credential mutation occurred. Remediation of the actual source findings continued. Resolution: resolved; next diagnostic on recurrence is pipeline exit/status interpretation, not retrying a publication operation. +- C10-b, audit recurrence at `2026-09-06T01:22:13Z`: a grouped source-context/CI metadata read exited 0, + but the connector explicitly reported `Warning: truncated output (original token count: 4181)` against + a 2800-token display request. Original stream bytes unavailable; do not treat the preview as a full read. + Effect **partial** inspection, **known-no-effect** on files/PR. A narrow exact-term search exited 0 + (183 tokens), followed by connector `read` of the known fixture lines 975-1068, recovered the needed + evidence without repeating a write. Disposition **resolved**; no blocked dependant remains. ### C8 — Standard check entrypoint cannot launch Apple's catalog parser +- Observation time: implementation-turn check, exact timestamp **not supplied**; repeated during this + 2026-09-06 UTC audit before repair commit `4885a8414`. Phase: required native verification. +- Action/shape: connector `exec_command`, `make check`; repeat via fixed-command Python capture that + prints only exit, byte counts/digests, and selected failure summaries. Wrapper exit 0 is not Make success. +- Effect: **known-no-effect** on intended native validation beyond entering the checker; no installation or + account mutation. Portable alternatives **desired-effect-observed**. Disposition: **open** (missing `plutil`). - Intended effect/action: run the required repository entrypoint `make check`. - Native result: exit 2 (Make recipe error 1); Node reported `spawnSync plutil ENOENT`, errno -2, no child PID/status/output. The bounded response was 328 tokens. @@ -203,6 +380,15 @@ The historical foundation evidence above is not evidence for this new candidate. ### C9 — A later inspection process could no longer resolve ripgrep +- Observation time: implementation-turn core review before commit `425b0a8ea`; exact timestamp **not supplied**. + Phase: complete-diff and fixture inspection. Native classification: Bash command resolution failure; + exit 127; no narrower connector error code returned. +- Output limits: original stdout/stderr bytes unavailable; 2931 rendered tokens include a successful preceding + staged diff. Effect: **partial** inspection, **known-no-effect** on files/Git; fallback **desired-effect-observed**. +- Disposition: **resolved by workaround**, cause of availability drift unknown; this is not proof that `rg` + was repaired. Unaffected code, fixture, catalog, documentation and publication work continued. +- Blocked dependants: the remaining searches in that one grouped invocation only; alternate inspection + completed them. No current project task requires ripgrep. - Intended effect/action: inspect the staged core diff, list fixture tests, and find overlong Swift lines. - Safe invocation: read-only `git diff --cached` followed by repository-scoped `rg` commands. - Native result: staged diff returned normally; then Bash reported `rg: command not found`, exit 127. @@ -211,14 +397,22 @@ The historical foundation evidence above is not evidence for this new candidate. remains preserved. The later searches did not execute because the wrapper used `set -eu`. - Workaround: use Git's built-in grep or standard grep for the remaining read-only inspection. Native Swift checks were already unavailable independently; implementation and publication are unaffected. -- Resolution: fallback inspection pending confirmation. Next diagnostic: command availability in the same - bound workspace, without changing PATH, installing utilities, or inspecting credentials. +- Next diagnostic on recurrence: command availability in the same bound workspace, without changing PATH, + installing utilities, or inspecting credentials. Successful fallback evidence follows. - Resolution evidence: `git grep` completed the full in-scope long-line audit with exit 0. Standard Git, grep, sed, and Python resolved successfully. The environmental cause of missing `rg` remains unknown, but no remaining task depends on it. ### C7 — Inline locale maintenance could not obtain platform safety status +- Observation time: implementation-turn localization, before helper execution at the catalog checkpoint; + exact rejected-call times **not supplied**. Phase: append-only locale registration and its verification. +- Native status/classification/code: the recorded indeterminate safety-status block below; no enum, process + status, stream sizes, or session returned. C7-a was the edit, C7-b/c the two read-only inspection attempts. +- Effect/disposition: intended edit initially **unknown**, reconciled to **known-no-effect**; helper's catalog + update **desired-effect-observed**. **Resolved** without assuming that a platform rejection means exit 0. +- Evidence limits: rejected payloads and absent native envelopes are not reconstructed; original rejection + is retained from this contemporaneous Work Note. Successful read/patch/run evidence is preserved below. - Intended effect: register missing feature strings as explicit English fallbacks without overwriting translations. - Actions / safe shapes: connector `exec_command` with relative-path inline Python catalog edits; then read-only Python and shell-loop key inspection. Three invocations returned the same @@ -236,6 +430,12 @@ The historical foundation evidence above is not evidence for this new candidate. ### C4 — Inspection glob did not match an existing source file +- Observation time: implementation-turn source inspection; exact timestamps **not supplied**. Phase: identify + relevant cache/localization code. Action family: connector `exec_command` searches and `read` ranges. +- Native classification: source selection error, not access denial. Effect: **partial** inspection and + **known-no-effect** on repository content. Exact source readbacks **desired-effect-observed**; **resolved**. +- Output limits: first grouped response `original_token_count=3108`; separate `rg` exit/stream byte sizes + unavailable. Recurrence returned exit 2 / 86 tokens. No private source excerpts or unbounded output retained. - Intended effect/action: read-only search for account-scoped token-cache invalidation using `rg`. - Safe shape: `rg Sources/CodexBar/UsageStore+Account* ...` inside the repository. - Native evidence: `rg: ...UsageStore+Account*: No such file or directory (os error 2)`; @@ -247,9 +447,23 @@ The historical foundation evidence above is not evidence for this new candidate. - Recurrence: a later read-only localization search included nonexistent `Sources/CodexBarCore/Localization*`, returned exit 2 (86 tokens), while identifying the actual app `Sources/CodexBar/Localization.swift`; that exact file was then read successfully. No mutation or blocked work. +- C4-c range-selection recurrence: connector `read` applied lines 230-275 to both `Scripts/lint.sh` and + `Sources/CodexBar/SettingsStore.swift`; the former returned `no lines in that range; the file has 229`, + while the latter returned the intended section. No process exit applies to that file-read response; + returned byte counts were not supplied. A subsequent complete `Scripts/lint.sh` read returned lines 1-229. Native test/CI inspection + and implementation proceeded; smallest next diagnostic on recurrence is the known file's actual line range. ### C5 — Native test discovery cannot launch Swift +- Phase: local required native compilation/test discovery. +- Observation time: implementation-turn native verification; exact timestamp **not supplied**; repeated during + this 2026-09-06 UTC audit before repair commit `4885a8414`. Action: connector `exec_command`, `make test`. +- Native classification/code: Python missing-executable `FileNotFoundError`, `Errno 2`; wrapper/Make exit 2, + Make recipe error 1, no Swift process exit because it did not start. +- Effect: **known-no-effect** on native test execution and live accounts. Source/fixture/portable verification + continued. Disposition: **open**, local Swift absent; CI can supply separate platform evidence. +- Output limits: original separate streams unavailable; audit rerun provides actual byte counts below, without + reconstructing the first invocation's counts. A successful diagnostic-capture wrapper is not a passing test. - Intended effect/action: run the repository's isolated native test entrypoint, `make test`. - Native result: connector command exit 2; Make reported test recipe error 1; Python raised `FileNotFoundError: [Errno 2] No such file or directory: 'swift'`. @@ -267,20 +481,31 @@ The historical foundation evidence above is not evidence for this new candidate. ### C6 — Repository Git author identity is unset +- Observation time: implementation-turn pre-commit verification; exact timestamp **not supplied**. + Phase: safely attribute this agent's coherent checkpoint. Connector action: `exec_command`. +- Native classification: Git identity/configuration error, exit 128; no narrower connector code. + Original stdout/stderr bytes unavailable; reported 3461 tokens cover the whole grouped inspection. +- Effect: failed read **known-no-effect**; attributed commit **desired-effect-observed**. Disposition: + **resolved by command-scoped identity**; no owner impersonation, global configuration write, or reset. - Intended effect/action: verify commit attribution with `git var GIT_AUTHOR_IDENT`. - Native result: exit 128, `Author identity unknown` / `fatal: empty ident name ... not allowed`. The surrounding inspection response was 3461 reported tokens; host-derived fallback identity is omitted. - Effect/postcondition: this read did not create a commit or modify Git configuration. - Workaround: use command-scoped `-c user.name='ChatGPT' -c user.email='chatgpt-agent@example.invalid'` for this agent's commits. Do not impersonate the owner or change global configuration. -- Blocked dependants: commit attribution only. Implementation and checks continued. Resolution will be - confirmed by the first successful attributed commit; next diagnostic is `git show` author/HEAD metadata. +- Blocked dependants: commit attribution only. Implementation and checks continued. The successful commit + evidence follows; next diagnostic on recurrence is `git show` author/HEAD metadata. - Resolution evidence: command-scoped identity produced commit `425b0a8ea64c992ed6d2254b56f4e715260d9249` with author `ChatGPT `. No global Git setting was changed. ### C3 — Edit invocation had indeterminate platform safety status +- Observation time: implementation-turn validator work; exact timestamp **not supplied**. + Phase: typed response and freshness implementation. Native block below is preserved from the contemporaneous + Work Note; original rejection envelope is not available for richer classification or byte accounting. +- Effect: requested edit **unknown** until original-file readback, then **known-no-effect**; authorized patch + **desired-effect-observed**. Disposition: **resolved**. No process exit or effect is inferred from silence. - Intended effect: refactor CAAM snapshot validation and freshness gating in one repository file. - Action / safe invocation: connector `exec_command`, relative-path Python text edit of `Sources/CodexBarCore/Providers/Codex/CAAM/CAAMEnvironmentValidation.swift`. @@ -296,6 +521,10 @@ The historical foundation evidence above is not evidence for this new candidate. ### C1 — Virtual path rejected before command launch +- Observation time: initial implementation-turn workspace verification; exact timestamp **not supplied**. + Phase: preflight before substantive edits. Original rejection is preserved from the contemporaneous Work Note; + its diagnostic byte count is unavailable. `No command was run` is explicit evidence, not a guessed exit status. +- Effect: original **known-no-effect**, corrected verification **desired-effect-observed**. Disposition: **resolved**. - Intended effect: read-only identity, ownership, Git-state, and remote verification. - Action / safe invocation: connector `exec_command`, bound workspace (handle omitted), `workdir: /project`, command containing `stat /project` plus read-only Git queries. @@ -312,6 +541,14 @@ The historical foundation evidence above is not evidence for this new candidate. ### C2 — Bundled read-only inspection rejected by platform safety +- Observation time: implementation-turn dependency research, exact timestamp **not supplied**. + Phase: tooling/source discovery and pinned dependency verification. Original block text is retained from + the contemporaneous Work Note, not a freshly retrieved native rejection envelope. +- Native classification/code: platform safety rejection as quoted below; no narrower code, process exit, + byte sizes, or command session available. One bundled attempt; no equivalent external retry/bypass. +- Effect: **known-no-effect** for requested repository writes (none requested); execution and external read + result unobserved. CodexBar-only alternative **desired-effect-observed**. Disposition: **open** for pinned + CAAM evidence, not for repository access. No credential recovery qualification was inspected or re-proved. - Intended effect: enumerate in-repository tooling/tests/guidance and read the exact external CAAM commit. - Action / safe invocation: connector `exec_command` with relative repository searches, `command -v`, and a conditional `gh api repos/Jones-Systems/CAAM/commits/` read. @@ -326,6 +563,8 @@ The historical foundation evidence above is not evidence for this new candidate. - Resolution: repository inspection recovered; external dependency remains unverified. Next diagnostic: owner-side platform rejection details, not broader credentials or recovery-bundle inspection. +## Implementation and verification evidence + ### Implementation reconciliation - The existing client executes only snapshots; mutation argv exists but typed plans/results and @@ -441,7 +680,7 @@ reasons to withhold the independent implementation from a draft PR. wrap long touched lines, and require valid fixture UTF-8. The bounded diagnostic retrieval issue is C10 above. The same draft PR receives the remediation commit and normal push; CI will rerun for its exact HEAD. -### Final delivery verification +### Original delivery verification — updated by the diagnostic audit - Remediation commit `76616f4b8cfea3aa65fc23c60a8992e0b5af365e` was normally pushed and verified as the head of draft PR #2. The replacement CI run is @@ -459,3 +698,34 @@ reasons to withhold the independent implementation from a draft PR. - Persistent incident evidence is this committed path, `docs/work-notes/caam-environment-control.md`, linked from . All incidents C1-C11 include bounded native evidence, effect classification, postconditions, workarounds, and remaining diagnostics. + +### Diagnostic audit and repair — 2026-09-06 UTC + +The follow-up audit inspected the existing Work Note and actual Git/PR/CI state rather than creating a +parallel report. It preserves C1-C11, adds the previously unnumbered native lint finding (C12), and records +the new helper block, fixture compilation failure, diagnostic-check corrections (C13-C15), and intentional +draft-policy aggregate gate (C16). +Missing original event times, byte counts, separate process/component exits, and native rejection envelopes +are explicit. Truncated previews and successful follow-up reads are distinguished. A historical private +absolute worktree path was redacted; no Git history rewrite was attempted. + +The actual CI failure yielded an independent in-scope repair: explicit `configuredPath: nil` in the relative +PATH fixture, commit `4885a8414396889ed799c4a35263154ceb5aa5a2`, normally pushed to the same draft PR. +Production code, the accepted wire contract, CI policy, external repositories, credentials and services +were unchanged in this follow-up. The diagnostic checkpoint after that repair changes only this Work Note. + +At the `2026-09-06T01:24:04Z` readback, repair run `34003546574` had passed lint and path detection; +Linux builds/tests and musl remained in progress, and macOS native tests were skipped by draft policy. +Unobserved native results are not treated as passing evidence. Portable helper tests (5), feature catalog +coverage (111 keys / 23 catalogs), documentation links (192), documentation index and whitespace checks +passed after the fixture repair. Local `make test` / `make check` remained blocked as C5/C8 quantify. + +Self-review of the complete diagnostic diff checked source attribution, known-versus-unknown effect claims, +original-versus-retry status, current dispositions, bounded/redacted evidence, and no expansion of authority. +This was one agent's sequential self-review, not independent review. The existing PR remains the delivery +vehicle: . + +Audit closure: at `2026-09-06T01:29:28Z`, both Linux native build/test/smoke jobs on the repair HEAD +were successful, closing C14. The musl build was still in progress; no final aggregate pass is claimed +at that observation. The latest field/redaction/size check passed at 63436 bytes before adding these +native outcomes; final whitespace and size checks are required before the diagnostic commit. From 2f4a3f7efe7540d9692d03d047f8f95c8b4faff8 Mon Sep 17 00:00:00 2001 From: Malcolm Jones Date: Wed, 7 Oct 2026 13:00:05 +0200 Subject: [PATCH 7/9] Backport inherited formatting fixes to the stack base Applies only the six-file source correction from ada1598a67642644ebb943d71c705950848ff0b9. This fixes macOS formatter diagnostics on PR4 run 37610484066 job112756366143 at their PR2 origin. Native stack10 remains 2 ->4 ->5 ->6; parent updates propagate this correction upward without individual merges or history rewriting. Git owner: thread718b02d7-0d4b-4670-b05f-57a5db28178d. make check blocked by missing plutil; make test blocked by missing Swift. Diff whitespace check passes; final hosted checks remain required. --- Sources/CodexBar/CAAMEnvironmentControlsView.swift | 4 +++- Sources/CodexBar/CodexObservabilityHubView.swift | 6 ++++-- .../PreferencesProvidersPane+Observability.swift | 4 ++-- .../Observability/ObservabilityHub.swift | 8 ++++++-- .../Codex/CAAM/CAAMOperationReceiptStore.swift | 4 +++- .../CAAMEnvironmentCoordinatorTests.swift | 12 ++++++++---- 6 files changed, 26 insertions(+), 12 deletions(-) diff --git a/Sources/CodexBar/CAAMEnvironmentControlsView.swift b/Sources/CodexBar/CAAMEnvironmentControlsView.swift index b20e1817db..35d5080357 100644 --- a/Sources/CodexBar/CAAMEnvironmentControlsView.swift +++ b/Sources/CodexBar/CAAMEnvironmentControlsView.swift @@ -6,7 +6,9 @@ struct CAAMEnvironmentControlsView: View { let configuration: CAAMEnvironmentConfiguration @Bindable var coordinator: CAAMEnvironmentCoordinator - private var session: CAAMControlSession { self.coordinator.session(for: self.configuration) } + private var session: CAAMControlSession { + self.coordinator.session(for: self.configuration) + } var body: some View { let session = self.session diff --git a/Sources/CodexBar/CodexObservabilityHubView.swift b/Sources/CodexBar/CodexObservabilityHubView.swift index c21ad7b058..c3e56afbf7 100644 --- a/Sources/CodexBar/CodexObservabilityHubView.swift +++ b/Sources/CodexBar/CodexObservabilityHubView.swift @@ -8,7 +8,9 @@ private enum ObservabilityHubTab: String, CaseIterable, Identifiable { case systems case work - var id: String { self.rawValue } + var id: String { + self.rawValue + } var title: String { switch self { @@ -153,7 +155,7 @@ struct CodexObservabilityHubView: View { format: L("Provider: %@ · Health: %@"), account.provider ?? L("Unknown"), account.health.rawValue)) - .font(.caption2) + .font(.caption2) if account.hostDefault { Text(L("Host default")).font(.caption2) } HubEvidenceView(evidence: account.evidence) } diff --git a/Sources/CodexBar/PreferencesProvidersPane+Observability.swift b/Sources/CodexBar/PreferencesProvidersPane+Observability.swift index 622789a87b..ee5481fe80 100644 --- a/Sources/CodexBar/PreferencesProvidersPane+Observability.swift +++ b/Sources/CodexBar/PreferencesProvidersPane+Observability.swift @@ -6,8 +6,8 @@ extension ProvidersPane { let provider = instanceID.firstPartyProvider let publication = self.store.tokenSnapshotPublications[instanceID] let cost: CostUsageTokenSnapshot? = if let provider, let publication, - publication.providerConfigRevision == - self.settings.providerConfigRevision(for: provider) + publication.providerConfigRevision == + self.settings.providerConfigRevision(for: provider) { publication.snapshot } else { diff --git a/Sources/CodexBarCore/Observability/ObservabilityHub.swift b/Sources/CodexBarCore/Observability/ObservabilityHub.swift index 7c8cdbb90c..226b368c37 100644 --- a/Sources/CodexBarCore/Observability/ObservabilityHub.swift +++ b/Sources/CodexBarCore/Observability/ObservabilityHub.swift @@ -48,7 +48,9 @@ public struct HubAccountObservation: Sendable, Equatable, Identifiable { public let health: CAAMProfileHealth public let evidence: HubEvidence - public var id: String { "\(self.environmentID):\(self.profile)" } + public var id: String { + "\(self.environmentID):\(self.profile)" + } /// Correlation never joins profile names or display emails across environments/providers. public func isSameAccount(as other: Self) -> Bool { @@ -63,7 +65,9 @@ public struct HubServiceObservation: Sendable, Equatable, Identifiable { public let pendingOperationID: UUID? public let evidence: HubEvidence - public var id: String { self.row.id } + public var id: String { + self.row.id + } } public struct HubCostObservation: Sendable, Equatable { diff --git a/Sources/CodexBarCore/Providers/Codex/CAAM/CAAMOperationReceiptStore.swift b/Sources/CodexBarCore/Providers/Codex/CAAM/CAAMOperationReceiptStore.swift index e9c86ffdcb..91daf73b05 100644 --- a/Sources/CodexBarCore/Providers/Codex/CAAM/CAAMOperationReceiptStore.swift +++ b/Sources/CodexBarCore/Providers/Codex/CAAM/CAAMOperationReceiptStore.swift @@ -22,7 +22,9 @@ public actor CAAMMemoryReceiptStore: CAAMOperationReceiptStoring { self.receipts = receipts } - public func load() -> [CAAMOperationReceipt] { self.receipts } + public func load() -> [CAAMOperationReceipt] { + self.receipts + } public func save(_ receipts: [CAAMOperationReceipt]) { self.receipts = receipts diff --git a/Tests/CodexBarTests/CAAMEnvironmentCoordinatorTests.swift b/Tests/CodexBarTests/CAAMEnvironmentCoordinatorTests.swift index d45e6cc93c..d8c814dbe2 100644 --- a/Tests/CodexBarTests/CAAMEnvironmentCoordinatorTests.swift +++ b/Tests/CodexBarTests/CAAMEnvironmentCoordinatorTests.swift @@ -120,7 +120,7 @@ private enum CAAMCoordinatorFixture { static func snapshot(active: String = "primary") -> CAAMEnvironmentSnapshot { CAAMEnvironmentSnapshot( revision: "1", - observedAt: Self.now, + observedAt: self.now, caamVersion: "fixture", capabilities: CAAMControlCapability.allCases.map(\.rawValue), reachability: .reachable, @@ -133,7 +133,7 @@ private enum CAAMCoordinatorFixture { runtime: CAAMRuntimeSnapshot(state: .unknown, reloadRequired: false)) } - static func response(_ value: Value, kind: String) throws -> SubprocessResult { + static func response(_ value: some Codable & Sendable, kind: String) throws -> SubprocessResult { let encoder = JSONEncoder() encoder.dateEncodingStrategy = .iso8601 let data = try encoder.encode(CAAMControlEnvelope( @@ -210,7 +210,11 @@ private actor GatedCAAMCoordinatorRunner: CAAMEnvironmentCommandRunning { } private actor RejectingCAAMReceiptStore: CAAMOperationReceiptStoring { - func load() -> [CAAMOperationReceipt] { [] } + func load() -> [CAAMOperationReceipt] { + [] + } - func save(_: [CAAMOperationReceipt]) throws { throw CAAMControlFailure(reason: .unavailable) } + func save(_: [CAAMOperationReceipt]) throws { + throw CAAMControlFailure(reason: .unavailable) + } } From a06dbadfa142cc30a8dc961079597832b4d6813a Mon Sep 17 00:00:00 2001 From: Malcolm Jones Date: Wed, 7 Oct 2026 13:02:03 +0200 Subject: [PATCH 8/9] Wrap the stack-base host observation identity property Addresses the remaining wrapPropertyBodies diagnostic in PR2 run37610566948 job112756867482. Whitespace-only source correction; local native checks remain unavailable because Swift and plutil are missing. Hosted current-head checks are required before native stack10 atomic merge. --- .../CodexBarCore/Observability/NetdataReadOnlyFacade.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Sources/CodexBarCore/Observability/NetdataReadOnlyFacade.swift b/Sources/CodexBarCore/Observability/NetdataReadOnlyFacade.swift index 1d41b65b04..f4f2e4c832 100644 --- a/Sources/CodexBarCore/Observability/NetdataReadOnlyFacade.swift +++ b/Sources/CodexBarCore/Observability/NetdataReadOnlyFacade.swift @@ -6,7 +6,9 @@ public struct HubHostObservation: Sendable, Equatable, Identifiable { public let cpuPercent: Double? public let memoryUsedBytes: UInt64? - public var id: String { self.environmentID } + public var id: String { + self.environmentID + } } /// No Netdata API contract is present in this repository. No network collector is activated or guessed. From 664dd64d8037c033bda6433bc8e6def8507e8b71 Mon Sep 17 00:00:00 2001 From: Malcolm Jones Date: Wed, 7 Oct 2026 13:40:12 +0200 Subject: [PATCH 9/9] Repair stack-base localization and architecture test anchors Backport the existing Italian labels from da78dc0cc62ea2ef591e1d266e8a5afb6f0282cc unchanged. Bind the three existing architecture allowlist entries to exact lower-prefix source lines318,334,347; upper Netdata bridge shifts these anchors five further lines, which must remain preserved there. Failing-before evidence: PR2 jobs112759620668/112759620626 and PR4 jobs112759619063/112759618985. No allowlist reason, fingerprint, reference count, policy or production behavior change. Native Swift/plutil checks unavailable locally; whitespace and direct anchor checks pass; current-head hosted CI remains required. --- .../Resources/it.lproj/Localizable.strings | 148 +++++++++--------- .../ProviderArchitectureGatekeeperTests.swift | 6 +- 2 files changed, 77 insertions(+), 77 deletions(-) diff --git a/Sources/CodexBar/Resources/it.lproj/Localizable.strings b/Sources/CodexBar/Resources/it.lproj/Localizable.strings index 950baca773..052ac46811 100644 --- a/Sources/CodexBar/Resources/it.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/it.lproj/Localizable.strings @@ -1566,77 +1566,77 @@ "SSH destination" = "Destinazione SSH"; "Update required" = "Aggiornamento necessario"; -/* CAAM observability: English fallback pending translation review. */ -"%@ · Recorded session %d" = "%@ · Recorded session %d"; -"Affected profiles" = "Affected profiles"; -"An unresolved operation belongs to a changed environment. Restore its original configuration." = "An unresolved operation belongs to a changed environment. Restore its original configuration."; -"Available" = "Available"; -"CAAM may complete or roll back its pending transaction. This does not restore fallback." = "CAAM may complete or roll back its pending transaction. This does not restore fallback."; -"CAAM operation tracking is unavailable. Mutations remain disabled." = "CAAM operation tracking is unavailable. Mutations remain disabled."; -"CAAM requires manual intervention on this environment. Automatic recovery is disabled." = "CAAM requires manual intervention on this environment. Automatic recovery is disabled."; -"Change host default: %@ → %@" = "Change host default: %@ → %@"; -"Confirm once" = "Confirm once"; -"Cost unavailable; not zero." = "Cost unavailable; not zero."; -"Dashboard URL (not stored)" = "Dashboard URL (not stored)"; -"Data confidence: %@" = "Data confidence: %@"; -"Estimated API cost: %@" = "Estimated API cost: %@"; -"Expected revision: %@" = "Expected revision: %@"; -"Gateway code: %@" = "Gateway code: %@"; -"Historical provider cache; not a billing receipt or an identified CAAM account." = "Historical provider cache; not a billing receipt or an identified CAAM account."; -"Host default" = "Host default"; -"Host default: %@" = "Host default: %@"; -"Host resources" = "Host resources"; -"Last operation: %@ · %@" = "Last operation: %@ · %@"; -"List-price estimate" = "List-price estimate"; -"Local service state, provider usage and cost, and host resources are separate ledgers." = "Local service state, provider usage and cost, and host resources are separate ledgers."; -"Look up operation" = "Look up operation"; -"Mixed provenance" = "Mixed provenance"; -"Netdata telemetry is unsupported here. No host metrics were collected." = "Netdata telemetry is unsupported here. No host metrics were collected."; -"No account profiles have been observed." = "No account profiles have been observed."; -"No observation timestamp" = "No observation timestamp"; -"No provider observations are available." = "No provider observations are available."; -"No session details are loaded. Use the existing provider refresh to collect supported data." = "No session details are loaded. Use the existing provider refresh to collect supported data."; -"Observability hub" = "Observability hub"; -"Observability view" = "Observability view"; -"Observed: %@" = "Observed: %@"; -"Open an existing Netdata dashboard" = "Open an existing Netdata dashboard"; -"Open dashboard in browser" = "Open dashboard in browser"; -"Opening a dashboard does not import telemetry or verify this host." = "Opening a dashboard does not import telemetry or verify this host."; -"Operation outcome is unresolved. Look up the existing operation before another change." = "Operation outcome is unresolved. Look up the existing operation before another change."; -"Partial" = "Partial"; -"Partial view: limited to 100 recorded sessions." = "Partial view: limited to 100 recorded sessions."; -"Plan digest: %@" = "Plan digest: %@"; -"Plan fallback" = "Plan fallback"; -"Plan switch" = "Plan switch"; -"Primary window: %.1f%% used" = "Primary window: %.1f%% used"; -"Profiles are environment-local. Only provider and stable ID establish cross-environment identity." = "Profiles are environment-local. Only provider and stable ID establish cross-environment identity."; -"Provider usage and API cost" = "Provider usage and API cost"; -"Provider: %@ · Health: %@" = "Provider: %@ · Health: %@"; -"Read-only until CAAM conformance is independently qualified." = "Read-only until CAAM conformance is independently qualified."; -"Recorded cost: %@ · %d days · %@" = "Recorded cost: %@ · %d days · %@"; -"Recorded local sessions only; not a live job queue or evidence of running processes." = "Recorded local sessions only; not a live job queue or evidence of running processes."; -"Refresh and resolve pending operations before changing this environment." = "Refresh and resolve pending operations before changing this environment."; -"Resets: %@" = "Resets: %@"; -"Resolve retained operations before adding more environment observations." = "Resolve retained operations before adding more environment observations."; -"Retained unresolved operation" = "Retained unresolved operation"; -"Review fallback restoration" = "Review fallback restoration"; -"Review interrupted-operation recovery" = "Review interrupted-operation recovery"; -"Review recovery" = "Review recovery"; -"Review switch plan" = "Review switch plan"; -"Runtime effective profile: %@" = "Runtime effective profile: %@"; -"Runtime state: %@" = "Runtime state: %@"; -"Separate metered amount: %@ (not added)" = "Separate metered amount: %@ (not added)"; -"Service and local state" = "Service and local state"; -"Stale" = "Stale"; -"Status and recovery remain bound to the original environment configuration." = "Status and recovery remain bound to the original environment configuration."; -"Systems" = "Systems"; -"The CAAM configuration is invalid." = "The CAAM configuration is invalid."; -"Tokens: %d" = "Tokens: %d"; -"Unknown provenance" = "Unknown provenance"; -"Unresolved operation: %@" = "Unresolved operation: %@"; -"Usage unavailable" = "Usage unavailable"; -"Use HTTPS, or loopback HTTP, without credentials, query parameters, or fragments." = "Use HTTPS, or loopback HTTP, without credentials, query parameters, or fragments."; -"Vendor-metered" = "Vendor-metered"; -"Waiting for CAAM; the app remains available." = "Waiting for CAAM; the app remains available."; -"Window length: %d minutes" = "Window length: %d minutes"; -"Work" = "Work"; +/* CAAM observability. */ +"%@ · Recorded session %d" = "%@ · Sessione registrata %d"; +"Affected profiles" = "Profili interessati"; +"An unresolved operation belongs to a changed environment. Restore its original configuration." = "Un’operazione irrisolta appartiene a un ambiente modificato. Ripristina la configurazione originale."; +"Available" = "Disponibile"; +"CAAM may complete or roll back its pending transaction. This does not restore fallback." = "CAAM può completare o annullare la transazione in sospeso. Questo non ripristina la riserva."; +"CAAM operation tracking is unavailable. Mutations remain disabled." = "Il monitoraggio delle operazioni CAAM non è disponibile. Le modifiche restano disabilitate."; +"CAAM requires manual intervention on this environment. Automatic recovery is disabled." = "CAAM richiede un intervento manuale in questo ambiente. Il ripristino automatico è disabilitato."; +"Change host default: %@ → %@" = "Cambia il profilo predefinito dell’host: %@ → %@"; +"Confirm once" = "Conferma una volta"; +"Cost unavailable; not zero." = "Costo non disponibile; non equivale a zero."; +"Dashboard URL (not stored)" = "URL della dashboard (non memorizzato)"; +"Data confidence: %@" = "Affidabilità dei dati: %@"; +"Estimated API cost: %@" = "Costo API stimato: %@"; +"Expected revision: %@" = "Revisione prevista: %@"; +"Gateway code: %@" = "Codice gateway: %@"; +"Historical provider cache; not a billing receipt or an identified CAAM account." = "Cache storica del provider; non è una ricevuta di fatturazione né un account CAAM identificato."; +"Host default" = "Profilo predefinito dell’host"; +"Host default: %@" = "Profilo predefinito dell’host: %@"; +"Host resources" = "Risorse dell’host"; +"Last operation: %@ · %@" = "Ultima operazione: %@ · %@"; +"List-price estimate" = "Stima basata sui prezzi di listino"; +"Local service state, provider usage and cost, and host resources are separate ledgers." = "Lo stato del servizio locale, l’utilizzo e i costi del provider e le risorse dell’host sono registri separati."; +"Look up operation" = "Cerca l’operazione"; +"Mixed provenance" = "Provenienza mista"; +"Netdata telemetry is unsupported here. No host metrics were collected." = "La telemetria Netdata non è supportata qui. Non sono state raccolte metriche dell’host."; +"No account profiles have been observed." = "Non sono stati osservati profili di account."; +"No observation timestamp" = "Nessun orario di osservazione"; +"No provider observations are available." = "Non sono disponibili osservazioni del provider."; +"No session details are loaded. Use the existing provider refresh to collect supported data." = "Non sono stati caricati dettagli delle sessioni. Usa l’aggiornamento esistente del provider per raccogliere i dati supportati."; +"Observability hub" = "Centro di osservabilità"; +"Observability view" = "Vista di osservabilità"; +"Observed: %@" = "Osservato: %@"; +"Open an existing Netdata dashboard" = "Apri una dashboard Netdata esistente"; +"Open dashboard in browser" = "Apri la dashboard nel browser"; +"Opening a dashboard does not import telemetry or verify this host." = "L’apertura di una dashboard non importa telemetria né verifica questo host."; +"Operation outcome is unresolved. Look up the existing operation before another change." = "L’esito dell’operazione è irrisolto. Cerca l’operazione esistente prima di un’altra modifica."; +"Partial" = "Parziale"; +"Partial view: limited to 100 recorded sessions." = "Vista parziale: limitata a 100 sessioni registrate."; +"Plan digest: %@" = "Digest del piano: %@"; +"Plan fallback" = "Pianifica la riserva"; +"Plan switch" = "Pianifica il cambio"; +"Primary window: %.1f%% used" = "Finestra principale: utilizzo %.1f%%"; +"Profiles are environment-local. Only provider and stable ID establish cross-environment identity." = "I profili sono locali all’ambiente. Solo il provider e l’ID stabile identificano il profilo tra ambienti diversi."; +"Provider usage and API cost" = "Utilizzo del provider e costo API"; +"Provider: %@ · Health: %@" = "Provider: %@ · Stato: %@"; +"Read-only until CAAM conformance is independently qualified." = "Sola lettura finché la conformità di CAAM non viene verificata in modo indipendente."; +"Recorded cost: %@ · %d days · %@" = "Costo registrato: %@ · %d giorni · %@"; +"Recorded local sessions only; not a live job queue or evidence of running processes." = "Solo sessioni locali registrate; non è una coda di attività in tempo reale né una prova di processi in esecuzione."; +"Refresh and resolve pending operations before changing this environment." = "Aggiorna e risolvi le operazioni in sospeso prima di modificare questo ambiente."; +"Resets: %@" = "Reimpostazione: %@"; +"Resolve retained operations before adding more environment observations." = "Risolvi le operazioni conservate prima di aggiungere altre osservazioni dell’ambiente."; +"Retained unresolved operation" = "Operazione irrisolta conservata"; +"Review fallback restoration" = "Esamina il ripristino della riserva"; +"Review interrupted-operation recovery" = "Esamina il ripristino dell’operazione interrotta"; +"Review recovery" = "Esamina il ripristino"; +"Review switch plan" = "Esamina il piano di cambio"; +"Runtime effective profile: %@" = "Profilo effettivo del runtime: %@"; +"Runtime state: %@" = "Stato del runtime: %@"; +"Separate metered amount: %@ (not added)" = "Importo misurato separatamente: %@ (non sommato)"; +"Service and local state" = "Servizio e stato locale"; +"Stale" = "Non aggiornato"; +"Status and recovery remain bound to the original environment configuration." = "Lo stato e il ripristino restano associati alla configurazione originale dell’ambiente."; +"Systems" = "Sistemi"; +"The CAAM configuration is invalid." = "La configurazione CAAM non è valida."; +"Tokens: %d" = "Token: %d"; +"Unknown provenance" = "Provenienza sconosciuta"; +"Unresolved operation: %@" = "Operazione irrisolta: %@"; +"Usage unavailable" = "Utilizzo non disponibile"; +"Use HTTPS, or loopback HTTP, without credentials, query parameters, or fragments." = "Usa HTTPS o HTTP di loopback, senza credenziali, parametri di query o frammenti."; +"Vendor-metered" = "Misurato dal fornitore"; +"Waiting for CAAM; the app remains available." = "In attesa di CAAM; l’app resta disponibile."; +"Window length: %d minutes" = "Durata della finestra: %d minuti"; +"Work" = "Lavoro"; diff --git a/Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift b/Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift index 88d1ff9511..dc5d4dcc26 100644 --- a/Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift +++ b/Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift @@ -2277,7 +2277,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact preferences test fixture seeds representative provider versions, snapshots, and accounts."), AllowedProviderConstruct( path: "Sources/CodexBar/PreferencesProvidersPane.swift", - line: 300, + line: 318, anchor: "guard let state = self.codexAccountsSectionState(for: .codex), state.canAddAccount else {", expectedProviderIDs: ["codex"], expectedReferenceCount: 1, @@ -2285,7 +2285,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact app-runtime bridge coordinates provider-owned state through the shared controller."), AllowedProviderConstruct( path: "Sources/CodexBar/PreferencesProvidersPane.swift", - line: 316, + line: 334, anchor: "guard let state = self.codexAccountsSectionState(for: .codex), state.canReauthenticate(account) else {", expectedProviderIDs: ["codex"], expectedReferenceCount: 1, @@ -2293,7 +2293,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact app-runtime bridge coordinates provider-owned state through the shared controller."), AllowedProviderConstruct( path: "Sources/CodexBar/PreferencesProvidersPane.swift", - line: 329, + line: 347, anchor: "guard let state = self.codexAccountsSectionState(for: .codex), state.canReauthenticate(account) else {", expectedProviderIDs: ["codex"], expectedReferenceCount: 1,