Replace the custom encrypted container with AgeSwift 0.1.0 so standard
age tools can inspect sensitive configuration independently of the app.
Use independent Keychain X25519 identities for catalog.age and
proxy-listening.age. Preserve atomic commits and confirmed recovery;
reject historical NG2 formats without migration or cleanup.
The root system-proxy helper logged nothing and launchd discards its
stdout/stderr, leaving helper launches, connection refusals, GUI-side
timeouts and root-side failures invisible. Share the minimal unified-log
emission unit (SystemLog) across GUI, wrapper and helper, and add a
dedicated closed helper event enum (lifecycle minimum: listener ready,
connection accepted/rejected with peer pid, typed apply/clear results)
with ADR-0045 privacy splitting. The emission seam is injected at the
helper engine and listener for test doubles. Helper events are
Console-only: no stderr, no agent.log, no diagnostic view or report
surface (ADR-0047).
Collapse the four definitions of "one log line" into LogLine (timestamp,
level, source, text) with a single renderer and parser. agent.log lines
now carry the uniform prefix, absorbing the ssxng: event marker into the
source column; legacy lines degrade to raw display without rewriting.
Level detection moves into the row model with a classic three-level
taxonomy whose unified-log projection stays unchanged.
The diagnostics log view merges GUI events and the agent.log tail through
DiagnosticLogIntake into one time-ordered list with level and source
filters; the tail window logic moves into the intake and blank lines are
kept for raw fidelity. The report whitelist projection and unified-log
routing are unchanged.
Add the SSXNG_SYSTEM_LOG=0 test seam: RuntimeLog.emit and the sslocal
unified-log destination skip their os.Logger writes when it is set,
while stderr lines and the event sink are unaffected. Test execution
no longer floods the unified log store (a full validate run previously
recorded 795 entries under the shared subsystem, 743 from xctest and
52 from test-spawned agents).
The switch is injected by the schemes' test actions only (Run/Profile
keep system logging so debugging from Xcode still shows it), and set
explicitly in the fixtures that spawn the agent/sslocal processes,
including the fixture that strips inherited SSXNG_* path seams. The
main scheme is now declared explicitly to keep the variable out of the
Launch/Profile actions. Document the seam in ADR-0045.
Record ADR-0045: unified-log routing through the emission seam with
native os.Logger and no third-party logging library, the closed-enum
dispatch with public/private text split, and the sslocal output intake
as a second free-form stream that never enters RuntimeLogEvent. Point
the systemLogRoute comment at the ADR.
Separate Agent supervision from sslocal input and preserve the latest intent after deployment failure. Persist failure barriers, serialize publication and retirement, and verify reload and single-instance behavior with real processes.
Refs #83
Make systemProxyConfiguration total so a runtime document can no longer
swallow derivation via try? and silently skip desired system proxy
convergence. Delete the unreachable ProxyModeError / .mode failure fact
and its presentation; port validity stays at the settings store and the
document contract.
CatalogEntry gains createdAt/updatedAt and the catalog document moves to
v5 (reads v1-v5, writes v5). Modification time is structure-sensitive:
editing a node's own fields bumps the node, a group's direct child set
or order changes bump the group, and a moved node keeps its timestamps.
No-op writes and unchanged subscription refreshes bump nothing; the
refresh rebuild compares per node, so surviving leaves keep theirs. All
times are passed in by callers as now: Date - one moment per logical
commit, the domain layer stays clock-free. Legacy documents load with
nil (unknown, never backfilled); duplicates and imports stamp fresh
times at the moment of the operation.
Semantics are recorded as glossary term 节点时间戳 and ADR 0031. No UI
in this change.
Implements issue #71 with ADR-0020 (supersedes ADR-0019's endpoint-signature
cleanup, startup cleanup, and health-gate cleanup).
- New ShadowsocksX-NG2SystemProxyHelper target: app-bundled root LaunchDaemon
(SMAppService.daemon, on-demand via MachServices) that mechanically applies
a closed typed configuration or executes an unconditional clear. Clients are
accepted by code-signing bundle identifier only; requests run serially on a
shared queue (last-writer-wins).
- SystemProxyConfiguration is now the closed typed apply payload (Codable over
JSON XPC): explicit SOCKS/HTTP/HTTPS enable + endpoints, PAC and
auto-discovery disabled with URL/JS removed, explicit simple-hostname
exclusion, complete ExceptionsList. Unmodeled Proxies keys are preserved and
equivalent values skip the commit.
- Cleanup is a side effect of a persisted system-proxy intent on-to-off
transition only: the switch itself, or the agent-off cascade (persist both
intents off, clear, then stop listeners; failed cascade persistence records
no transition and clears nothing). Launch with the switch off, health-gate
closure, invalid targets, and passive events never clear. Failed clears are
surfaced without deferred retry.
- Endpoint-signature store, ownership migration, and direct Authorization
Services writes are removed; old signature files stay inert. Helper
unavailability or pending approval keeps an enabled intent pending and
surfaces the Login Items approval path (HomeView button + menu label).
Migrate SilentLaunchStore from its versioned JSON file to the app's
standard defaults domain (key `silentLaunch`), the platform's canonical
home for GUI-scalar preferences, so the GUI preference category stops
growing one file per item. Reads stay safe-side strict: a missing key
or a value not bridgeable to Bool loads as not silent (plain
bool(forKey:) would coerce "yes" to true). The file-form
persistence-failure naming path is dropped — UserDefaults.set reports
no failure — so SilentLaunchController keeps its persist-then-flip seam
while errorMessage and its SettingsView presentation go away.
The legacy file never shipped in any release; migrateLegacyFileIfPresent
runs in the production composition root only, preserves the value on
developer machines, and deletes the file (no-op when absent). The test
composition uses a dedicated suite cleared at assembly; unit tests use
per-test suites with removePersistentDomain teardown, never the
standard domain.
ADR-0017 carries a dated amendment (Status plus the affected
consequence line); the settings.json separation rationale stands
unchanged.
- switch to regular while the workspace window is open (Dock icon,
app switcher, default menu bar) and back to accessory when it closes;
driven by NSWindow didBecomeKey/willClose with an anchor-view identity
filter (scenePhase follows the app rather than windows on macOS, so it
fires no event on close)
- add silent launch preference (settings-general, off by default,
persisted in its own silent-launch.json since settings.json is rewritten
whole from the controller's in-memory snapshot): suppressed launches
keep the menu-bar baseline and proxy runtime restoration is unaffected
- docs: CONTEXT.md adaptive app form / silent launch terms and invariants;
ADR 0017 superseding 0016 (scene-based model and reopening carry over)
Replace the hybrid form (LSUIElement + runtime activation-policy
switching + AppKit-owned NSWindow) with a plain SwiftUI Window scene:
the app stays accessory at all times, launch presents the workspace
window via defaultLaunchBehavior(.presented), and the status menu
reopens it after close via openWindow. Drop AppDelegate and the
window-opening adapter and intents; WorkspaceRoute keeps navigation
state only, with the scene id defined there. Documented in ADR 0016
and CONTEXT.md.