From 6838fb4e69a76167e295c24015832f0f5bb3ac60 Mon Sep 17 00:00:00 2001 From: 0xLeif Date: Sat, 18 Jul 2026 20:42:07 -0600 Subject: [PATCH 01/17] Encrypted-file secret store via swift-crypto (Closes #35) - new SecretStore.swift: age-style envelope (ephemeral X25519 ECDH + HKDF + ChaCha20-Poly1305, AlgoChat construction directly on swift-crypto) under the state root; ciphertext at rest, tri-OS, zero prompts - unlock model: key file /secret.key (0600) by default, APS_SECRET_PASSPHRASE opt-in via HKDF-SHA256, TTY getpass fallback with APS_SECRET_USE_PASSPHRASE=1 - replaces the Keychain SecureState backend (per-access password prompts on ad-hoc signed binaries): SecureState extension and APSKeychain removed, dead keychainUnavailable case removed - write-then-read-back verification kept; freshValue reads the store directly so cross-process writes surface in watch; wrong key fails loud (secretUnlockFailed), corrupt envelope fails decodingFailed - tests (6 new/changed, skips removed) + smoke (round-trip, perms, reset, passphrase right/wrong); specs/README/GOAL/windows-readiness in lockstep - SpecSync CHG-0021 accepted (research + 2 requirements); evidence rebound for shared changes (audited reopens, actor 0xLeif) --- .specsync/change-sequence.json | 4 +- .../approvals.json | 19 ++ .../change.md | 25 ++ .../context.md | 25 ++ .../deltas/aps-cli.md | 57 ++++ .../deltas/state-store.md | 38 +++ .../design.md | 21 ++ .../docs.md | 13 + .../plan.md | 15 ++ .../requirements.md | 12 + .../research.md | 37 +++ .../state.json | 52 ++++ .../tasks.md | 11 + .../testing.md | 18 ++ .../verification-attempts.json | 23 ++ .../verification.json | 254 ++++++++++++++++++ GOAL.md | 2 +- Package.resolved | 20 +- Package.swift | 6 +- README.md | 20 +- Scripts/smoke.sh | 31 +-- Sources/aps/Aps.swift | 2 +- Sources/aps/DemoKey.swift | 35 +-- Sources/aps/DemoState.swift | 13 - Sources/aps/SecretStore.swift | 220 +++++++++++++++ Sources/aps/StateStore.swift | 47 ++-- Tests/apsTests/APSTests.swift | 87 +++--- docs/windows-readiness.md | 6 +- specs/aps-cli/aps-cli.spec.md | 21 +- specs/aps-cli/requirements.md | 18 +- specs/state-store/requirements.md | 6 +- specs/state-store/state-store.spec.md | 3 +- 32 files changed, 1009 insertions(+), 152 deletions(-) create mode 100644 .specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/approvals.json create mode 100644 .specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/change.md create mode 100644 .specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/context.md create mode 100644 .specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/deltas/aps-cli.md create mode 100644 .specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/deltas/state-store.md create mode 100644 .specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/design.md create mode 100644 .specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/docs.md create mode 100644 .specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/plan.md create mode 100644 .specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/requirements.md create mode 100644 .specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/research.md create mode 100644 .specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/state.json create mode 100644 .specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/tasks.md create mode 100644 .specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/testing.md create mode 100644 .specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/verification-attempts.json create mode 100644 .specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/verification.json create mode 100644 Sources/aps/SecretStore.swift diff --git a/.specsync/change-sequence.json b/.specsync/change-sequence.json index 66369b1..2cbe868 100644 --- a/.specsync/change-sequence.json +++ b/.specsync/change-sequence.json @@ -1,6 +1,6 @@ { "schema_version": 1, - "sequence": 23, - "id": "CHG-0023-watch-signal-handling-and-termination-semantics-issue-34", + "sequence": 24, + "id": "CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35", "acknowledged_collisions": [] } diff --git a/.specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/approvals.json b/.specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/approvals.json new file mode 100644 index 0000000..bcef8b0 --- /dev/null +++ b/.specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/approvals.json @@ -0,0 +1,19 @@ +{ + "approvals": [ + { + "gate": "definition", + "actor": "0xLeif", + "timestamp": 1784427354, + "digest": "827ed26145641b8b0d33b4e64e3404a86c75fa2f53393feaeead8250d493964d", + "note": null + }, + { + "gate": "acceptance", + "actor": "0xLeif", + "timestamp": 1784427376, + "digest": "3d67077ab2a07e353bc39ae80ac266a16b556db7a242601a9178111d94706cc5", + "note": null + } + ], + "reopenings": [] +} diff --git a/.specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/change.md b/.specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/change.md new file mode 100644 index 0000000..0918dba --- /dev/null +++ b/.specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/change.md @@ -0,0 +1,25 @@ +--- +id: CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35 +state: accepted +type: feature +base_commit: c4ecd60b2a46bca4780f1b4af79a1d495d646287 +--- + +# Encrypted-file secret store via swift-crypto (issue 35) + +## Intent + +Encrypted-file secret store via swift-crypto (issue 35) + +## Affected Canonical Specs + +- `aps-cli` +- `state-store` + +## Acceptance Criteria + +- secret round-trips set/get/reset through an encrypted-file store under the state root (ephemeral X25519 + HKDF + ChaCha20-Poly1305 via swift-crypto, ciphertext at rest, key file mode 0600); zero interactive prompts in key-file mode; passphrase mode via APS_SECRET_PASSPHRASE with loud secretUnlockFailed on wrong key; corrupt envelope fails decodingFailed; no Security.framework imports remain; keychainUnavailable case removed; cross-process watch reads the store directly; tests and smoke cover all of it on macOS and Linux. + +## No-spec Rationale + +Not applicable diff --git a/.specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/context.md b/.specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/context.md new file mode 100644 index 0000000..7eb27e1 --- /dev/null +++ b/.specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/context.md @@ -0,0 +1,25 @@ +--- +change: CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35 +artifact: context +--- + +# Context + +Issue #35 (design decision 3ii). The Keychain-backed `secret` prompted for +the macOS password on every access: an ad-hoc signed CLI can never earn +durable Keychain trust, and SecureState says nothing for Windows/Linux. +The fix is an encrypted-file store under the state root, prompt-free and +tri-OS. + +## Decisions + +- swift-crypto directly (no AlgoKit): ephemeral X25519 ECDH + HKDF + + ChaCha20-Poly1305, AlgoChat's Crypto/ as the reference construction. +- Custom JSON envelope (ephemeral public key, nonce, ciphertext, tag as + base64); age interop deferred to a later export/import. +- Unlock model: key file `/secret.key` (0600) by default; + `APS_SECRET_PASSPHRASE` opt-in via HKDF-SHA256; TTY getpass fallback + when `APS_SECRET_USE_PASSPHRASE=1`. Agents always non-interactive. +- AppState `SecureState` and `APSKeychain` are removed; the dead + `keychainUnavailable` case goes with them (the #42 lesson applied). +- Write-then-read-back verification kept from the #27 pattern. diff --git a/.specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/deltas/aps-cli.md b/.specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/deltas/aps-cli.md new file mode 100644 index 0000000..e159f62 --- /dev/null +++ b/.specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/deltas/aps-cli.md @@ -0,0 +1,57 @@ +# aps-cli encrypted-file secret store + +## MODIFIED + +### REQUIREMENT REQ-aps-cli-005 + +`APSError` SHALL cover `invalidValue`, `encodingFailed`, `decodingFailed`, `persistenceFailed`, `keychainUnavailable`, and `corruptState`. + +Acceptance Criteria +- Each case has an actionable `description`. +- `set note` surfaces `persistenceFailed` when the on-disk value does not match after write. +- `corruptState` is used when a FileState file exists but cannot be decoded. + +### REQUIREMENT REQ-aps-cli-020 + +The `secret` key SHALL be backed by an encrypted-file secret store under the state root (ephemeral X25519 + HKDF + ChaCha20-Poly1305 via swift-crypto), with zero interactive prompts in key-file mode and passphrase mode via `APS_SECRET_PASSPHRASE`. + +Acceptance Criteria +- `secret` round-trips set/get/reset with ciphertext at rest in `secret.enc`; the key file is mode 0600. +- No Security.framework/Keychain imports; works on macOS and Linux. +- Wrong passphrase fails with `APSError.secretUnlockFailed`; corrupt envelope fails with `APSError.decodingFailed`. +- Passphrase entry is env-var based; an optional TTY getpass prompt exists when `APS_SECRET_USE_PASSPHRASE=1`. + +### SPEC SECTION Public API + +| Export | Description | +|--------|-------------| +| `DemoKey` | Fixed schema enum including `profile`, `secret`, and `profileName`. | +| `ProfileDocument` | Codable `{name, version}` FileState document. | +| `name` | ProfileDocument display name field. | +| `version` | ProfileDocument integer version field. | +| `init` | ProfileDocument memberwise initializer. | +| `APSError` | Typed CLI/domain errors. | +| `counter` | Int key stored in AppState `State`. | +| `message` | String key stored in AppState `State`. | +| `flag` | Bool key stored in AppState `StoredState`. | +| `note` | String key stored in AppState `FileState`. | +| `profile` | ProfileDocument key stored in AppState `FileState`. | +| `secret` | String key stored in the encrypted-file secret store (`secret.enc`). | +| `SecretStore` | Encrypted-file store: `get` / `set` / `reset` over `secret.enc`. | +| `hasSecret` | True when a store file exists (missing means initial value). | +| `get` | Decrypt the stored secret; loud corrupt/unlock failures. | +| `set` | Encrypt, persist, and read-back verify. | +| `reset` | Delete `secret.enc`, restoring the initial value. | +| `profileName` | String Slice over `ProfileDocument.name`. | +| `invalidValue` | Value could not parse for the key type. | +| `encodingFailed` | UTF-8 JSON encode failure. | +| `decodingFailed` | UTF-8 JSON decode failure. | +| `persistenceFailed` | Disk-backed key did not persist after write. | +| `secretUnlockFailed` | Secret store would not open (wrong passphrase or key). | +| `corruptState` | FileState file exists but is undecodable (torn write). | +| `corruptStateExitCode` | Exit code 65 (`EX_DATAERR`) for `corruptState`. | +| `storage` | Human storage kind (`State` / `StoredState` / `FileState` / `EncryptedFile` / `Slice`). | +| `valueType` | Human value type (`Int` / `String` / `Bool` / `ProfileDocument`). | +| `helpSummary` | Tab-separated key/type/storage columns for `keys`. | +| `detail` | One-line description for `keys`. | +| `description` | Actionable error text for humans and ValidationError bridging. | diff --git a/.specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/deltas/state-store.md b/.specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/deltas/state-store.md new file mode 100644 index 0000000..076c4c2 --- /dev/null +++ b/.specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/deltas/state-store.md @@ -0,0 +1,38 @@ +# state-store encrypted secret paths + +## MODIFIED + +### SPEC SECTION Public API + +| Export | Description | +|--------|-------------| +| `StateStore` | MainActor facade over demo AppState keys. | +| `init` | Loads clock/jsonCoding/stats dependencies without forcing `~/.aps`. | +| `get` | Returns the current string rendering for a demo key. | +| `set` | Parses and writes a demo key value; records a stats mutation. | +| `reset` | Restores one demo key to its initial value; records a stats mutation. | +| `resetAll` | Restores every demo key. | +| `dump` | Pretty JSON snapshot with typed values. | +| `watchBlocking` | Observation + polling watch loop for demo keys. | +| `watchStatsBlocking` | Combine + polling watch loop for ObservedDependency stats. | +| `statsSnapshot` | Immutable view of DemoStats counters. | +| `resetStats` | Clears process-local DemoStats counters. | +| `profileDocument` | Typed profile FileState accessor. | +| `profileName` | Slice accessor for ProfileDocument.name. | +| `readNoteFromDisk` | Direct `note.json` read requiring a present decodable file. | +| `readNoteFromDiskIfPresent` | Optional `note.json` read; throws `corruptState` if torn. | +| `readProfileFromDisk` | Direct `profile.json` read requiring a present decodable file. | +| `readProfileFromDiskIfPresent` | Optional `profile.json` read; throws `corruptState` if torn. | +| `requireDecodableDiskState` | Loud-fail helper for CLI get/watch on FileState keys. | +| `parseBool` | Bool token parser for flag values. | +| `APSClock` | Injected clock dependency protocol. | +| `now` | APSClock current instant. | +| `SystemAPSClock` | Date-backed clock. | +| `JSONCoding` | Shared encode helpers for dump output. | +| `encodePretty` | Pretty JSON encode helper. | +| `DemoStats` | ObservableObject mutation-stats dependency. | +| `mutationCount` | Number of recorded set/reset mutations. | +| `lastMutatedKey` | Raw demo key of the latest mutation. | +| `recordMutation` | Increments counters for a demo key. | +| `reset` | Clears DemoStats counters. | +| `DemoStatsSnapshot` | Codable snapshot of DemoStats. | diff --git a/.specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/design.md b/.specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/design.md new file mode 100644 index 0000000..7fc95d3 --- /dev/null +++ b/.specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/design.md @@ -0,0 +1,21 @@ +--- +change: CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35 +artifact: design +--- + +# Design + +- New `Sources/aps/SecretStore.swift`: `get` / `set` / `reset` / + `hasSecret` over `secret.enc`. Missing file means the initial value; + corrupt envelope throws `decodingFailed`; a valid envelope that does + not open throws the new `APSError.secretUnlockFailed`. +- Recipient key: env passphrase (HKDF-SHA256, domain-separated salt/info) + or generated key file (base64 X25519 raw, chmod 0600 on create). +- Envelope: fresh ephemeral pair per write; shared secret through + HKDF-SHA256 ("aps-secret-store-v1" salt, "envelope" info). +- StateStore: `get` reads the store (initial on missing), `set` encrypts + and read-back verifies, `reset` deletes the file; `freshValue(.secret)` + reads directly so cross-process writes surface in watch; + `requireDecodableDiskState` covers secret with loud corrupt/unlock + failures. +- Package.swift adds apple/swift-crypto from 3.0.0. diff --git a/.specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/docs.md b/.specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/docs.md new file mode 100644 index 0000000..5282b1d --- /dev/null +++ b/.specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/docs.md @@ -0,0 +1,13 @@ +--- +change: CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35 +artifact: docs +--- + +# Docs + +README replaces the SecureState/Keychain section with "Encrypted-file +secret store (`secret`)": construction, key file vs passphrase modes, +TTY opt-in prompt, reset semantics, and the rationale for replacing +Keychain (prompt per access; AppState unchanged, SecureState dogfood +moves to AppStateExamples). Demo-keys table and AppState coverage matrix +updated; GOAL.md secret bullet and docs/windows-readiness.md rows updated. diff --git a/.specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/plan.md b/.specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/plan.md new file mode 100644 index 0000000..15164dc --- /dev/null +++ b/.specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/plan.md @@ -0,0 +1,15 @@ +--- +change: CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35 +artifact: plan +--- + +# Plan + +1. Add swift-crypto to Package.swift; write SecretStore.swift (envelope, + recipient key modes, get/set/reset). +2. Wire StateStore secret paths (get/set/reset/freshValue/requireDecodable); + remove SecureState extension, APSKeychain, keychainUnavailable case. +3. Tests: round-trip, reset deletes file, 0600 perms, corrupt envelope, + cross-instance persistence, passphrase round-trip + wrong key. +4. Smoke: key-file round-trip, perms, reset, passphrase right/wrong. +5. Specs (both modules), README, GOAL, windows-readiness in lockstep. diff --git a/.specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/requirements.md b/.specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/requirements.md new file mode 100644 index 0000000..d050926 --- /dev/null +++ b/.specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/requirements.md @@ -0,0 +1,12 @@ +--- +change: CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35 +artifact: requirements +--- + +# Requirements + +- New REQ-aps-cli-020: encrypted-file secret store (construction, zero + prompts, loud wrong-key and corrupt failures, tri-OS, no + Security.framework imports). +- REQ-aps-cli-005 error coverage swaps the removed `keychainUnavailable` + for `secretUnlockFailed`. diff --git a/.specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/research.md b/.specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/research.md new file mode 100644 index 0000000..9279dbd --- /dev/null +++ b/.specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/research.md @@ -0,0 +1,37 @@ +--- +change: CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35 +artifact: research +--- + +# Research + +## Keychain prompt root cause + +macOS Keychain grants durable item access per code signature. The `aps` +binary is ad-hoc signed in dev and its cdhash changes per build, so the +login Keychain re-prompts on every access; "Always Allow" cannot stick. +A stable Developer ID signature helps releases but not dev builds, and +says nothing for Windows/Linux. This matches the owner's report and the +pre-existing `APS_SMOKE_SECURESTATE=1` opt-out in smoke. + +## Crypto construction survey + +- AlgoChat `Sources/AlgoChat/Crypto/MessageEncryptor.swift` and + `EphemeralKeyManager.swift`: pure swift-crypto (X25519 ephemeral ECDH + + HKDF + ChaCha20-Poly1305, forward secrecy). Directly reusable as the + reference construction. +- The `AlgoChat` library target depends on AlgoKit (Algorand SDK) via + `KeyDerivation.swift`; taking the package as-is would drag a blockchain + SDK with a dubious Windows story into aps. Decision 3ii (owner): + depend on apple/swift-crypto directly and re-implement the same + envelope (~1 file). swift-crypto is the tri-OS-safe base (BoringSSL + everywhere; CryptoKit on Apple platforms). +- age/rage interop was considered and deferred: the store is consumed by + aps itself; interop can arrive later as export/import. + +## Passphrase vs key file + +- Key file (0600) matches the SSH model: zero prompts, headless-safe, + security equals filesystem permissions. +- Passphrase via env var keeps agents non-interactive; a TTY getpass + prompt exists only as an opt-in fallback, never required. diff --git a/.specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/state.json b/.specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/state.json new file mode 100644 index 0000000..137cf74 --- /dev/null +++ b/.specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/state.json @@ -0,0 +1,52 @@ +{ + "schema_version": 1, + "id": "CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35", + "slug": "encrypted-file-secret-store-via-swift-crypto-issue-35", + "title": "Encrypted-file secret store via swift-crypto (issue 35)", + "description": "Encrypted-file secret store via swift-crypto (issue 35)", + "kind": "feature", + "state": "accepted", + "canonical_applied": true, + "base_commit": "c4ecd60b2a46bca4780f1b4af79a1d495d646287", + "created_at": 1784427325, + "updated_at": 1784427376, + "affected_specs": [ + "aps-cli", + "state-store" + ], + "affected_paths": [ + "Package.swift", + "Package.resolved", + "Sources/aps/SecretStore.swift", + "Sources/aps/DemoKey.swift", + "Sources/aps/DemoState.swift", + "Sources/aps/StateStore.swift", + "Tests/apsTests/APSTests.swift", + "Scripts/smoke.sh", + "specs", + "README.md", + "GOAL.md", + "docs/windows-readiness.md", + ".specsync/change-sequence.json" + ], + "no_spec_change": false, + "no_spec_change_rationale": null, + "acceptance_criteria": [ + "secret round-trips set/get/reset through an encrypted-file store under the state root (ephemeral X25519 + HKDF + ChaCha20-Poly1305 via swift-crypto, ciphertext at rest, key file mode 0600); zero interactive prompts in key-file mode; passphrase mode via APS_SECRET_PASSPHRASE with loud secretUnlockFailed on wrong key; corrupt envelope fails decodingFailed; no Security.framework imports remain; keychainUnavailable case removed; cross-process watch reads the store directly; tests and smoke cover all of it on macOS and Linux." + ], + "selected_artifacts": [ + "context", + "requirements", + "plan", + "tasks", + "testing", + "docs", + "design", + "research" + ], + "dependencies": [], + "answers": { + "architecture_risk": "yes", + "public_contract": "yes" + } +} diff --git a/.specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/tasks.md b/.specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/tasks.md new file mode 100644 index 0000000..2b94cb2 --- /dev/null +++ b/.specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/tasks.md @@ -0,0 +1,11 @@ +--- +change: CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35 +artifact: tasks +--- + +# Tasks + +- [x] swift-crypto dependency and SecretStore.swift +- [x] StateStore secret paths rewired; SecureState/APSKeychain removed +- [x] Unit tests and smoke assertions +- [x] Specs, README, GOAL, windows-readiness updates diff --git a/.specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/testing.md b/.specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/testing.md new file mode 100644 index 0000000..66b6097 --- /dev/null +++ b/.specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/testing.md @@ -0,0 +1,18 @@ +--- +change: CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35 +artifact: testing +--- + +# Testing + +## Requirement evidence + +| Requirement | Evidence | +| --- | --- | +| REQ-aps-cli-005 | `testAPSErrorDescriptionsAreActionable` plus `testSecretPassphraseRoundTripAndWrongKey` covering `secretUnlockFailed` | +| REQ-aps-cli-020 | `testSecretEncryptedStoreRoundTrip`, `testSecretResetDeletesStoreFile`, `testSecretKeyFilePermissionsAre0600`, `testSecretStoreCorruptEnvelopeThrowsDecodingFailed`, `testSecretPersistsAcrossStateStoreInstances`, `testSecretPassphraseRoundTripAndWrongKey`; smoke: key-file round-trip, 0600 perms, reset, passphrase right/wrong | + +## Suites + +- `swift test` +- `fledge lanes run verify` diff --git a/.specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/verification-attempts.json b/.specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/verification-attempts.json new file mode 100644 index 0000000..c2f5eb0 --- /dev/null +++ b/.specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/verification-attempts.json @@ -0,0 +1,23 @@ +{ + "schema_version": 1, + "attempts": [ + { + "timestamp": 1784427370, + "commit": "c4ecd60b2a46bca4780f1b4af79a1d495d646287", + "contract_digest": "827ed26145641b8b0d33b4e64e3404a86c75fa2f53393feaeead8250d493964d", + "workspace_digest": "8fa57eaecb8655afb8788e481cb5ea90ade16f2ea9979f71e9a13d66b44e14c1", + "passed": true, + "commands": [ + { + "command": "fledge lanes run verify", + "success": true, + "exit_code": 0 + } + ], + "requirement_ids": [ + "REQ-aps-cli-005", + "REQ-aps-cli-020" + ] + } + ] +} diff --git a/.specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/verification.json b/.specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/verification.json new file mode 100644 index 0000000..7f0d59a --- /dev/null +++ b/.specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/verification.json @@ -0,0 +1,254 @@ +{ + "timestamp": 1784427370, + "commit": "c4ecd60b2a46bca4780f1b4af79a1d495d646287", + "contract_digest": "827ed26145641b8b0d33b4e64e3404a86c75fa2f53393feaeead8250d493964d", + "workspace_digest": "8fa57eaecb8655afb8788e481cb5ea90ade16f2ea9979f71e9a13d66b44e14c1", + "acceptance_input_digest": "11de5fb1ae0313091894fb39437ec2a85ba7a434f61bc48cdb7814894aa55ae6", + "acceptance_manifest": { + "schema_version": 1, + "entries": [ + { + "path": ".specsync/change-sequence.json", + "kind": "file", + "mode": 33188, + "payload_digest": "59a0e39b3046172391fffe9fade542bec84123a2ed01006eb3d2c8eb983b3281", + "entry_digest": "253eb565ab4faed6063db30553febe5c53bcac9fff6e7da4908bfd654abcc0ad", + "owners": [ + "@exact:delivery" + ] + }, + { + "path": "GOAL.md", + "kind": "file", + "mode": 33188, + "payload_digest": "1f202b1275871514d91e71bf71089add50fd54b7b6a01e4c2758f37ea7d7de13", + "entry_digest": "063f926655d07fb9d5f645ac5f092920c81ab092aaee19106d431da7b0a96820", + "owners": [ + "@exact:delivery" + ] + }, + { + "path": "Package.resolved", + "kind": "file", + "mode": 33188, + "payload_digest": "1da38ca760f1067d66c2055b0e6356376643a2747e7b0b782c441e82a1412df3", + "entry_digest": "16e2671c61546e6acf924975b46d72191cf49aa3bb3fa50af98c26928bd42e37", + "owners": [ + "@exact:delivery" + ] + }, + { + "path": "Package.swift", + "kind": "file", + "mode": 33188, + "payload_digest": "fa5c94630e24811d9fb6ba5c3670cbfd608c227ca569d20a6f9bf86e26e110a6", + "entry_digest": "300ea3cc8908fc5f97a879f503b249d389547d01e089a41b9dee33f259e777a6", + "owners": [ + "@exact:delivery" + ] + }, + { + "path": "README.md", + "kind": "file", + "mode": 33188, + "payload_digest": "6fef4366c759779d0eb2514b5c0ed919f68252a057bee450ff43d38f4e3ec0ea", + "entry_digest": "f0caa4bb27ab04a23cd111e69283fcc9460e82c91a47816b8e73eb167f159644", + "owners": [ + "@exact:delivery" + ] + }, + { + "path": "Scripts/smoke.sh", + "kind": "file", + "mode": 33261, + "payload_digest": "7bbfe819c7a6b60ea891ab711ed314ad1ad53111920d17bc14499ea018cb7373", + "entry_digest": "4868e08df80f240a34d4b264f0ef1fb0f5201b4ac09600c11924aac3a38a4874", + "owners": [ + "@exact:delivery" + ] + }, + { + "path": "Sources/aps/DemoKey.swift", + "kind": "file", + "mode": 33188, + "payload_digest": "f361daf0e91573e911a97c6d713783fbdea923a397e83b333efde52b2c11b08c", + "entry_digest": "5df4f7ba4efcd3ac515674c67789235c0ae978f96aa013bc8f3fb7527cdbf4bf", + "owners": [ + "aps-cli" + ] + }, + { + "path": "Sources/aps/DemoState.swift", + "kind": "file", + "mode": 33188, + "payload_digest": "52511df78ff00e335d6154357fb927cb5761cf34fdec2d1c34b07945210eae8c", + "entry_digest": "c131e1cc18c9ddd59fd8c07bb7aa937ea5eacb0abc182811eb1ca09d7a75412e", + "owners": [ + "state-store" + ] + }, + { + "path": "Sources/aps/SecretStore.swift", + "kind": "file", + "mode": 33188, + "payload_digest": "f5247a42cdf94aae7893a24f81fc4608809f9d5f4fd9afb37f4b1e8d99bd7d6a", + "entry_digest": "2f7b32a90e4c70b1b9062201ffc935a66cb1fdfa060b5810d5b32995f916c429", + "owners": [ + "aps-cli" + ] + }, + { + "path": "Sources/aps/StateStore.swift", + "kind": "file", + "mode": 33188, + "payload_digest": "d5f0995ca8c1bbae4489d305de16d5264a486534a4ebdb16de0c9a9828e6c10d", + "entry_digest": "26bf7a564b76388fe42804bd641855168fc03dbd9503bc112b244de677f46c5e", + "owners": [ + "state-store" + ] + }, + { + "path": "Tests/apsTests/APSTests.swift", + "kind": "file", + "mode": 33188, + "payload_digest": "085adc68eb6c47c04b30d22df1bf8d62265593ce46beaccc16ff5dbbb00b7821", + "entry_digest": "330affb934181c0cde7bc76bdaf26c050583b473c9227ff0546d6049edb283f3", + "owners": [ + "@exact:test" + ] + }, + { + "path": "docs/windows-readiness.md", + "kind": "file", + "mode": 33188, + "payload_digest": "a807cbb47e76fb39f27b6136315147be654ae37b044b6f7d73e9dc1fed9b8176", + "entry_digest": "f8614191469e65f306d82023c07dea6c0e95a804380eef9c5d2cf981ed188fb3", + "owners": [ + "@exact:delivery" + ] + }, + { + "path": "specs", + "kind": "non_file", + "mode": 0, + "payload_digest": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "entry_digest": "d654cba9ce3c5b8a9c72964ac1f99b8ae3d9a6de6b54d1baa36d7558aac4bb42", + "owners": [ + "@exact:delivery" + ] + }, + { + "path": "specs/aps-cli/aps-cli.spec.md", + "kind": "file", + "mode": 33188, + "payload_digest": "26fe418a175baec4c451ea6011c3b4b426ea61150659c9f136e459a03fd24430", + "entry_digest": "b46d50ba4234978fe558af5ec14e47eae86cd4cce09f0c3dd5b1868b99ee8dc5", + "owners": [ + "aps-cli" + ] + }, + { + "path": "specs/aps-cli/context.md", + "kind": "file", + "mode": 33188, + "payload_digest": "7304a6233da2051242aeac63a51197aaf6f92d936692fe8655801eff842a2fd0", + "entry_digest": "e78ad0996c0135c37d41afa37c5fda13901bb63e14f00c3208896f7b104a72e8", + "owners": [ + "aps-cli" + ] + }, + { + "path": "specs/aps-cli/requirements.md", + "kind": "file", + "mode": 33188, + "payload_digest": "23bc812eaf3d419296e67ccf92fcb82f3f74c33e22785ba1c12c2bf239527c3a", + "entry_digest": "99b36c1f5c94a902692f4a507d3114735d20bdb0ed7dc7bb593c20155e60372e", + "owners": [ + "aps-cli" + ] + }, + { + "path": "specs/aps-cli/tasks.md", + "kind": "file", + "mode": 33188, + "payload_digest": "ca6cafd22fb3c803eed3dc94cb980d8f535d51e0fd289b46bae82b7f255377de", + "entry_digest": "c349b8f073cefe7e387f8fabdd4dada3830be59b4b6d22a4ce79b2c29399c70e", + "owners": [ + "aps-cli" + ] + }, + { + "path": "specs/aps-cli/testing.md", + "kind": "file", + "mode": 33188, + "payload_digest": "ee5ae7f63536e2089a2b51887ed62b8392561052cdaa37c29707e7a10e2136bd", + "entry_digest": "daf1e496e836c46da31eb6ac679b352d7bf615536595be310953eb0ae9a36fd4", + "owners": [ + "aps-cli" + ] + }, + { + "path": "specs/state-store/context.md", + "kind": "file", + "mode": 33188, + "payload_digest": "12d848cab584c8a64f7da8bee796b581ec3b1b5efc72448299330dd493f39aa7", + "entry_digest": "76b734c278c05e3e74ff8f41dbb07aac3dcc7b1fbda64adcf32691e31365317c", + "owners": [ + "state-store" + ] + }, + { + "path": "specs/state-store/requirements.md", + "kind": "file", + "mode": 33188, + "payload_digest": "6d94234b7a6b7d4dab2b432a46d1d59449941bd031b7e6e841a13d7830305ef1", + "entry_digest": "42a35cb8eed79a217a0016bc9ab3d9a5cc41b4c20f6f950e66edc40c21ac6d72", + "owners": [ + "state-store" + ] + }, + { + "path": "specs/state-store/state-store.spec.md", + "kind": "file", + "mode": 33188, + "payload_digest": "a12b268bf828a38a97bcb4b207167bb6b612329a2aa7545f2aa82dd461796833", + "entry_digest": "1e6f68725c14ed1d4bdabad641b987b6ba7998d7d1b04d596f721d80d51b9592", + "owners": [ + "state-store" + ] + }, + { + "path": "specs/state-store/tasks.md", + "kind": "file", + "mode": 33188, + "payload_digest": "0f02b782887467b358568bf22c2d68f39c40a00d56143bd6c64abdf8198cf433", + "entry_digest": "8f828f3625d4f24c3c71852bd28f76aac97cd31ace2a18d1a901a3d34642de8d", + "owners": [ + "state-store" + ] + }, + { + "path": "specs/state-store/testing.md", + "kind": "file", + "mode": 33188, + "payload_digest": "63705355034895d0dfab654892140beecd317afb08530917c8cf4b308824ab63", + "entry_digest": "88818028e56f72fbfd2bdf24012faf448fc049911bf77a2ecb1f32ba1c3e6389", + "owners": [ + "state-store" + ] + } + ] + }, + "passed": true, + "commands": [ + { + "command": "fledge lanes run verify", + "success": true, + "exit_code": 0 + } + ], + "requirement_ids": [ + "REQ-aps-cli-005", + "REQ-aps-cli-020" + ] +} diff --git a/GOAL.md b/GOAL.md index 47973c7..32ec60e 100644 --- a/GOAL.md +++ b/GOAL.md @@ -36,7 +36,7 @@ Make `aps` reliable enough for agents to inspect, mutate, and watch fixed demo A ## In scope (added) -- SecureState dogfood via the fixed `secret` demo key (Keychain-backed String). Available where Apple's Security framework exists (macOS). Keychain round-trip tests and smoke are opt-in (`APS_SMOKE_SECURESTATE=1`); default CI skips them to avoid Keychain prompts. Linux documents keychain-unavailable behavior. +- `secret` is backed by an encrypted-file store (ephemeral X25519 + HKDF + ChaCha20-Poly1305 via swift-crypto; issue #35), replacing the Keychain-backed SecureState demo which prompted on every access. Works tri-OS with zero prompts (key file default; `APS_SECRET_PASSPHRASE` opt-in). ## Tickets diff --git a/Package.resolved b/Package.resolved index 810ca4c..34d0287 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "26c8955179127e90c5b2750191b7ffa99d5d6a0c43b82a93fd3731334426e8e1", + "originHash" : "fa5c94630e24811d9fb6ba5c3670cbfd608c227ca569d20a6f9bf86e26e110a6", "pins" : [ { "identity" : "appstate", @@ -27,6 +27,24 @@ "revision" : "6a52f3251125d74daf04fcbd5e6f08a75d074382", "version" : "1.8.2" } + }, + { + "identity" : "swift-asn1", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-asn1.git", + "state" : { + "revision" : "a9a5efd40eaf558a2bcd48d64b1d1646be686008", + "version" : "1.7.1" + } + }, + { + "identity" : "swift-crypto", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-crypto", + "state" : { + "revision" : "95ba0316a9b733e92bb6b071255ff46263bbe7dc", + "version" : "3.15.1" + } } ], "version" : 3 diff --git a/Package.swift b/Package.swift index c8a46e8..6cad3b9 100644 --- a/Package.swift +++ b/Package.swift @@ -15,14 +15,16 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/0xLeif/AppState", from: "3.0.0"), - .package(url: "https://github.com/apple/swift-argument-parser", from: "1.5.0") + .package(url: "https://github.com/apple/swift-argument-parser", from: "1.5.0"), + .package(url: "https://github.com/apple/swift-crypto", from: "3.0.0") ], targets: [ .executableTarget( name: "aps", dependencies: [ .product(name: "AppState", package: "AppState"), - .product(name: "ArgumentParser", package: "swift-argument-parser") + .product(name: "ArgumentParser", package: "swift-argument-parser"), + .product(name: "Crypto", package: "swift-crypto") ] ), .testTarget( diff --git a/README.md b/README.md index 7ea685a..5010689 100644 --- a/README.md +++ b/README.md @@ -32,19 +32,21 @@ State root resolution: `--state-dir` > `APS_HOME` > `~/.aps`. | `flag` | `Bool` | `StoredState` | Persisted (`UserDefaults`; CLI calls `synchronize()` so Linux flushes) | | `note` | `String` | `FileState` | Persisted (`$APS_HOME/note.json`) | | `profile` | `{name,version}` | `FileState` | Persisted structured Codable (`$APS_HOME/profile.json`) | -| `secret` | `String` | `SecureState` | Persisted in Keychain (macOS); account `dev.leif.aps/secret` | +| `secret` | `String` | `EncryptedFile` | Persisted encrypted under the state root (`secret.enc`) | | `profileName` | `String` | `Slice` | Projection of `profile.name` via AppState Slice | Dynamic / user-declared keys are intentionally out of scope for 0.x. -### SecureState / Keychain (`secret`) +### Encrypted-file secret store (`secret`) -- AppState `SecureState` stores the value under Keychain account `dev.leif.aps/secret` (feature `dev.leif.aps`, id `secret`). -- `aps reset secret` (and `aps reset --all`) deletes that Keychain item; get then returns an empty string. -- **macOS:** works when the process can access the login Keychain (interactive sessions and typical self-hosted macOS CI). -- **Linux / headless without Security:** Apple's Security framework is unavailable. `aps keys` still lists `secret`, `get` returns empty, and `set secret` fails with a clear Keychain-unavailable error. -- **Default CI / smoke:** Keychain round-trips are skipped. Re-enable with `APS_SMOKE_SECURESTATE=1` (Darwin) when a login Keychain is available. -- **Headless macOS CI:** if the Keychain is locked or inaccessible, set/get may fail with persistence errors. Unlock or use a runner with an available login Keychain. +`secret` is backed by an age-style encrypted envelope under the state root (issue #35), not the Keychain: ephemeral X25519 ECDH + HKDF + ChaCha20-Poly1305 via [swift-crypto](https://github.com/apple/swift-crypto), the same construction as [AlgoChat](https://github.com/CorvidLabs/swift-algochat)'s message encryptor. + +- **`secret.enc`** holds the encrypted envelope (ephemeral public key, nonce, ciphertext, tag, base64 JSON). Nothing plaintext at rest. +- **Key file mode (default):** a recipient key is generated on first use at `/secret.key` (base64 X25519, mode 0600), like an SSH key. Zero prompts, works headless and in CI, on every OS. +- **Passphrase mode:** set `APS_SECRET_PASSPHRASE` to derive the key from a passphrase via HKDF-SHA256 (no key file). Wrong passphrases fail loudly with `secretUnlockFailed`. +- **Interactive opt-in:** with `APS_SECRET_USE_PASSPHRASE=1` on a TTY, aps prompts once itself (its own getpass prompt, not macOS Keychain's). +- `aps reset secret` deletes `secret.enc`; the key file is kept for future writes. +- The previous AppState `SecureState` / Keychain backend was replaced: ad-hoc signed CLI binaries can never earn durable Keychain trust, so every access prompted for a password. AppState itself is unchanged; SecureState remains dogfooded in [AppStateExamples](https://github.com/0xLeif/AppStateExamples). ### Dependencies @@ -239,7 +241,7 @@ See [`GOAL.md`](GOAL.md) for **aps 0.2.0**: agent-ready AppState dogfood harness | `State` | `counter`, `message` | Dogfooded | | `StoredState` | `flag` | Dogfooded | | `FileState` | `note`, `profile` | Dogfooded | -| `SecureState` | `secret` | Dogfooded (Keychain tests/smoke opt-in) | +| `SecureState` | (none) | Not dogfooded here; moved to AppStateExamples after the Keychain prompt issue (issue #35) | | `Slice` | `profileName` | Dogfooded | | `@AppDependency` | `clock`, `jsonCoding` | Dogfooded | | `@ObservedDependency` | `stats` / `aps stats` | Dogfooded | diff --git a/Scripts/smoke.sh b/Scripts/smoke.sh index 7e768c6..d27c2d5 100755 --- a/Scripts/smoke.sh +++ b/Scripts/smoke.sh @@ -39,22 +39,23 @@ test "$("$bin" get note)" = "smoke-note" "$bin" get profile --json | grep -q '"name":"smoke"' "$bin" get profile --json | grep -q '"version":2' -# SecureState / Keychain smoke temporarily disabled (Keychain prompts / hangs). -# Re-enable with: APS_SMOKE_SECURESTATE=1 -if [[ "${APS_SMOKE_SECURESTATE:-}" == "1" ]]; then - if [[ "$(uname -s)" == "Darwin" ]]; then - "$bin" set secret "smoke-secret" >/dev/null - test "$("$bin" get secret)" = "smoke-secret" - "$bin" get secret --json | grep -q '"storage":"SecureState"' - "$bin" reset secret >/dev/null - test -z "$("$bin" get secret)" - else - if "$bin" set secret "smoke-secret" >/dev/null 2>&1; then - echo "expected secret set to fail without Keychain" >&2 - exit 1 - fi - fi +# Encrypted-file secret store: key-file mode round-trip, perms, reset. +"$bin" set secret "smoke-secret" >/dev/null +test "$("$bin" get secret)" = "smoke-secret" +"$bin" get secret --json | grep -q '"storage":"EncryptedFile"' +test "$(stat -f "%Lp" "$APS_HOME/secret.key" 2>/dev/null || stat -c "%a" "$APS_HOME/secret.key")" = "600" +"$bin" reset secret >/dev/null +test -z "$("$bin" get secret)" +test ! -f "$APS_HOME/secret.enc" + +# Passphrase mode: right phrase works, wrong phrase fails with a loud error. +APS_SECRET_PASSPHRASE=smoke-pass "$bin" set secret "phrase-secret" >/dev/null +test "$(APS_SECRET_PASSPHRASE=smoke-pass "$bin" get secret)" = "phrase-secret" +if APS_SECRET_PASSPHRASE=wrong-pass "$bin" get secret >/dev/null 2>&1; then + echo "expected wrong passphrase to fail" >&2 + exit 1 fi +"$bin" reset secret >/dev/null # --state-dir overrides APS_HOME OTHER="$(mktemp -d "${TMPDIR:-/tmp}/aps-smoke-other.XXXXXX")" diff --git a/Sources/aps/Aps.swift b/Sources/aps/Aps.swift index f0bb243..46c926b 100644 --- a/Sources/aps/Aps.swift +++ b/Sources/aps/Aps.swift @@ -14,7 +14,7 @@ struct Aps: ParsableCommand { flag Bool StoredState (UserDefaults) note String FileState (~/.aps/note.json) profile ProfileDocument FileState (~/.aps/profile.json) - secret String SecureState (Keychain; macOS) + secret String EncryptedFile (~/.aps/secret.enc) profileName String Slice (profile.name via AppState Slice) State root: --state-dir > APS_HOME > ~/.aps diff --git a/Sources/aps/DemoKey.swift b/Sources/aps/DemoKey.swift index 629d0c3..c63ce40 100644 --- a/Sources/aps/DemoKey.swift +++ b/Sources/aps/DemoKey.swift @@ -17,7 +17,7 @@ public enum DemoKey: String, CaseIterable, ExpressibleByArgument, Sendable { case .counter, .message: return "State" case .flag: return "StoredState" case .note, .profile: return "FileState" - case .secret: return "SecureState" + case .secret: return "EncryptedFile" case .profileName: return "Slice" } } @@ -49,7 +49,7 @@ public enum DemoKey: String, CaseIterable, ExpressibleByArgument, Sendable { case .profile: return "Codable {name,version} via FileState (~/.aps/profile.json)" case .secret: - return "String via SecureState / Keychain (\(APSKeychain.secretAccount))" + return "String via encrypted file store (/secret.enc)" case .profileName: return "profile.name via AppState Slice over FileState ProfileDocument" } @@ -67,29 +67,12 @@ public struct ProfileDocument: Codable, Equatable, Sendable { } } -/// Well-known Keychain identity for the `secret` SecureState demo key. -/// -/// AppState stores SecureState under `Scope.key` (`feature/id`) as the Keychain -/// account (`kSecAttrAccount`). The feature string is the service-style namespace. -public enum APSKeychain: Sendable { - /// Reverse-DNS style service / feature namespace. - public static let service = "dev.leif.aps" - - /// Account id within the service namespace. - public static let account = "secret" - - /// Full Keychain account key used by AppState (`service/account`). - public static var secretAccount: String { - "\(service)/\(account)" - } -} - public enum APSError: Error, CustomStringConvertible, Equatable { case invalidValue(key: DemoKey, value: String) case encodingFailed case decodingFailed case persistenceFailed(key: DemoKey) - case keychainUnavailable + case secretUnlockFailed /// On-disk FileState exists but cannot be decoded (torn concurrent write). case corruptState(key: DemoKey) @@ -106,8 +89,8 @@ public enum APSError: Error, CustomStringConvertible, Equatable { return "Failed to decode value from UTF-8 JSON" case .persistenceFailed(let key): return "Failed to persist \(key.rawValue)" - case .keychainUnavailable: - return "SecureState (Keychain) is unavailable on this platform. The secret key requires macOS Keychain." + case .secretUnlockFailed: + return "Failed to unlock the secret store (wrong passphrase or key)" case .corruptState(let key): return "Corrupt or torn \(key.rawValue) state file (undecodable). Concurrent writers may have torn the file; reset the key or repair the file." } @@ -120,7 +103,7 @@ public enum APSError: Error, CustomStringConvertible, Equatable { case .encodingFailed: return "encoding_failed" case .decodingFailed: return "decoding_failed" case .persistenceFailed: return "persistence_failed" - case .keychainUnavailable: return "keychain_unavailable" + case .secretUnlockFailed: return "secret_unlock_failed" case .corruptState: return "corrupt_state" } } @@ -131,7 +114,7 @@ public enum APSError: Error, CustomStringConvertible, Equatable { switch self { case .invalidValue: return 64 // EX_USAGE case .decodingFailed, .corruptState: return APSError.corruptStateExitCode // EX_DATAERR - case .keychainUnavailable: return 69 // EX_UNAVAILABLE + case .secretUnlockFailed: return 69 // EX_UNAVAILABLE case .encodingFailed: return 70 // EX_SOFTWARE case .persistenceFailed: return 73 // EX_CANTCREAT } @@ -148,8 +131,8 @@ public enum APSError: Error, CustomStringConvertible, Equatable { return "A value or file is not valid JSON for its key; check the input or the state root (--state-dir / APS_HOME)." case .persistenceFailed: return "Check that the state root exists and is writable (--state-dir / APS_HOME)." - case .keychainUnavailable: - return "The secret key currently requires macOS Keychain; an encrypted-file store is planned (issue #35)." + case .secretUnlockFailed: + return "Check APS_SECRET_PASSPHRASE or the secret.key file under the state root." case .corruptState(let key): return "Reset the key (`aps reset \(key.rawValue)`) or repair the file under the state root." } diff --git a/Sources/aps/DemoState.swift b/Sources/aps/DemoState.swift index b21fc09..f9404d7 100644 --- a/Sources/aps/DemoState.swift +++ b/Sources/aps/DemoState.swift @@ -40,19 +40,6 @@ extension Application { ) } -#if canImport(Security) - /// Sensitive string via Keychain-backed `SecureState`. - /// - /// Keychain account: `APSKeychain.secretAccount` (`dev.leif.aps/secret`). - /// Initial value is `nil` so `reset` deletes the Keychain item. - var secret: SecureState { - secureState( - initial: nil, - feature: APSKeychain.service, - id: APSKeychain.account - ) - } -#endif /// Wall-clock used when stamping watch/dump output. var clock: Dependency { diff --git a/Sources/aps/SecretStore.swift b/Sources/aps/SecretStore.swift new file mode 100644 index 0000000..3ce44e8 --- /dev/null +++ b/Sources/aps/SecretStore.swift @@ -0,0 +1,220 @@ +import AppState +import Crypto +import Foundation + +/// Encrypted-file secret store (issue #35): replaces the Keychain-backed +/// `secret` with an age-style envelope under the state root. Ephemeral +/// X25519 ECDH + HKDF + ChaCha20-Poly1305 (AlgoChat construction, directly +/// on swift-crypto). Prompt-free for humans and agents; tri-OS. +/// +/// Unlock model (interview decision): +/// - `APS_SECRET_PASSPHRASE` set: the recipient key is derived from the +/// passphrase via HKDF-SHA256 (no key file involved). +/// - `APS_SECRET_USE_PASSPHRASE=1` on a TTY: one interactive prompt (our +/// getpass prompt, not macOS Keychain's), same derivation. +/// - Otherwise a key file at `/secret.key` (base64 raw X25519 +/// private key, mode 0600) is created on first use, like an SSH key. +public struct SecretStore: Sendable { + + /// On-disk envelope: one JSON object with base64 fields. + struct Envelope: Codable { + let ephemeralPublicKey: String + let nonce: String + let ciphertext: String + let tag: String + } + + private let directory: String + + /// Store rooted at the configured FileState path. + @MainActor + public init() { + self.directory = FileManager.defaultFileStatePath + } + + /// Store rooted at an explicit directory (tests, tooling). + public init(directory: String) { + self.directory = directory + } + + private var storeURL: URL { + URL(fileURLWithPath: directory).appendingPathComponent("secret.enc") + } + + private var keyFileURL: URL { + URL(fileURLWithPath: directory).appendingPathComponent("secret.key") + } + + // MARK: - Public API + + /// True when a secret is stored (missing file means the initial value). + public var hasSecret: Bool { + FileManager.default.fileExists(atPath: storeURL.path) + } + + /// Decrypt and return the stored secret. + /// Missing file throws `APSError.persistenceFailed`; an existing file that + /// does not parse throws `APSError.decodingFailed`; a valid envelope that + /// does not open throws `APSError.secretUnlockFailed` (wrong key). + public func get() throws -> String { + let data: Data + do { + data = try Data(contentsOf: storeURL) + } catch { + throw APSError.persistenceFailed(key: .secret) + } + let envelope: Envelope + do { + envelope = try JSONDecoder().decode(Envelope.self, from: data) + } catch { + throw APSError.decodingFailed + } + return try open(envelope) + } + + /// Encrypt and store the value, then verify by decrypting the file back. + public func set(_ value: String) throws { + try FileManager.default.createDirectory( + atPath: directory, + withIntermediateDirectories: true + ) + let envelope = try seal(value) + let data = try JSONEncoder().encode(envelope) + do { + try data.write(to: storeURL) + } catch { + throw APSError.persistenceFailed(key: .secret) + } + // Read-back verification (same discipline as FileState writes). + guard try get() == value else { + throw APSError.persistenceFailed(key: .secret) + } + } + + /// Reset to the initial value: the store file is deleted. + public func reset() { + try? FileManager.default.removeItem(at: storeURL) + } + + // MARK: - Envelope cryptography + + private func seal(_ value: String) throws -> Envelope { + let recipientPublic = try recipientKey().publicKey + let ephemeral = Curve25519.KeyAgreement.PrivateKey() + let symmetric = try deriveSymmetricKey(privateKey: ephemeral, publicKey: recipientPublic) + let nonce = ChaChaPoly.Nonce() + let sealedBox = try ChaChaPoly.seal( + Data(value.utf8), + using: symmetric, + nonce: nonce + ) + return Envelope( + ephemeralPublicKey: ephemeral.publicKey.rawRepresentation.base64EncodedString(), + nonce: nonce.withUnsafeBytes { Data($0) }.base64EncodedString(), + ciphertext: sealedBox.ciphertext.base64EncodedString(), + tag: sealedBox.tag.base64EncodedString() + ) + } + + private func open(_ envelope: Envelope) throws -> String { + guard + let ephemeralPublicData = Data(base64Encoded: envelope.ephemeralPublicKey), + let nonceData = Data(base64Encoded: envelope.nonce), + let ciphertext = Data(base64Encoded: envelope.ciphertext), + let tag = Data(base64Encoded: envelope.tag), + let ephemeralPublic = try? Curve25519.KeyAgreement.PublicKey(rawRepresentation: ephemeralPublicData), + let nonce = try? ChaChaPoly.Nonce(data: nonceData), + let box = try? ChaChaPoly.SealedBox(nonce: nonce, ciphertext: ciphertext, tag: tag) + else { + throw APSError.decodingFailed + } + let symmetric = try deriveSymmetricKey( + privateKey: recipientKey(), + publicKey: ephemeralPublic + ) + let plaintext: Data + do { + plaintext = try ChaChaPoly.open(box, using: symmetric) + } catch { + throw APSError.secretUnlockFailed + } + guard let value = String(data: plaintext, encoding: .utf8) else { + throw APSError.decodingFailed + } + return value + } + + private func deriveSymmetricKey( + privateKey: Curve25519.KeyAgreement.PrivateKey, + publicKey: Curve25519.KeyAgreement.PublicKey + ) throws -> SymmetricKey { + let sharedSecret = try privateKey.sharedSecretFromKeyAgreement(with: publicKey) + return sharedSecret.hkdfDerivedSymmetricKey( + using: SHA256.self, + salt: Data("aps-secret-store-v1".utf8), + sharedInfo: Data("envelope".utf8), + outputByteCount: 32 + ) + } + + // MARK: - Recipient key + + private func recipientKey() throws -> Curve25519.KeyAgreement.PrivateKey { + if let passphrase = ProcessInfo.processInfo.environment["APS_SECRET_PASSPHRASE"] { + guard !passphrase.isEmpty else { + throw APSError.secretUnlockFailed + } + return Self.keyFromPassphrase(passphrase) + } + if ProcessInfo.processInfo.environment["APS_SECRET_USE_PASSPHRASE"] == "1", + isatty(FileHandle.standardError.fileDescriptor) == 1, + let passphrase = Self.promptPassphrase() { + return Self.keyFromPassphrase(passphrase) + } + return try loadOrCreateKeyFile() + } + + static func keyFromPassphrase(_ passphrase: String) -> Curve25519.KeyAgreement.PrivateKey { + let derived = HKDF.deriveKey( + inputKeyMaterial: SymmetricKey(data: Data(passphrase.utf8)), + salt: Data("aps-secret-store-v1".utf8), + info: Data("x25519-key".utf8), + outputByteCount: 32 + ) + return try! Curve25519.KeyAgreement.PrivateKey(rawRepresentation: derived.withUnsafeBytes { Data($0) }) + } + + private static func promptPassphrase() -> String? { + #if os(Windows) + return nil + #else + FileHandle.standardError.write(Data("aps secret passphrase: ".utf8)) + guard let raw = getpass("") else { return nil } + let passphrase = String(cString: raw) + return passphrase.isEmpty ? nil : passphrase + #endif + } + + private func loadOrCreateKeyFile() throws -> Curve25519.KeyAgreement.PrivateKey { + if let data = try? Data(contentsOf: keyFileURL), + let raw = Data(base64Encoded: data), + let key = try? Curve25519.KeyAgreement.PrivateKey(rawRepresentation: raw) { + return key + } + let key = Curve25519.KeyAgreement.PrivateKey() + try FileManager.default.createDirectory( + atPath: directory, + withIntermediateDirectories: true + ) + do { + try key.rawRepresentation.base64EncodedData().write(to: keyFileURL) + try FileManager.default.setAttributes( + [.posixPermissions: 0o600], + ofItemAtPath: keyFileURL.path + ) + } catch { + throw APSError.persistenceFailed(key: .secret) + } + return key + } +} diff --git a/Sources/aps/StateStore.swift b/Sources/aps/StateStore.swift index 18936d5..59b70eb 100644 --- a/Sources/aps/StateStore.swift +++ b/Sources/aps/StateStore.swift @@ -61,11 +61,8 @@ public final class StateStore { case .profile: return (try? encodeProfile(Application.fileState(\.profile).value)) ?? "{\"name\":\"\",\"version\":0}" case .secret: -#if canImport(Security) - return Application.secureState(\.secret).value ?? "" -#else - return "" -#endif + // Encrypted-file store; a missing store file means the initial value. + return (try? SecretStore().get()) ?? "" case .profileName: return Application.slice(\.profile, \.name).value } @@ -125,16 +122,9 @@ public final class StateStore { throw APSError.persistenceFailed(key: .profile) } case .secret: -#if canImport(Security) - var state = Application.secureState(\.secret) - state.value = value - let stored = Application.dependency(\.keychain).get(APSKeychain.secretAccount) - guard stored == value else { - throw APSError.persistenceFailed(key: .secret) - } -#else - throw APSError.keychainUnavailable -#endif + // Encrypted-file store (issue #35): age-style envelope under the + // state root; no Keychain, no prompts. Write then read-back verify. + try SecretStore().set(value) case .profileName: // Refresh FileState from disk before Slice write so a stale cached // ProfileDocument cannot clobber a newer on-disk version. @@ -164,11 +154,7 @@ public final class StateStore { case .profile: Application.reset(fileState: \.profile) case .secret: -#if canImport(Security) - Application.reset(secureState: \.secret) -#else - break -#endif + SecretStore().reset() case .profileName: try? Self.refreshProfileFileStateFromDisk() var slice = Application.slice(\.profile, \.name) @@ -304,7 +290,13 @@ public final class StateStore { return document.name } return get(key) - case .counter, .message, .flag, .secret: + case .secret: + // Encrypted store is file-backed: read it directly so cross-process + // writes surface (there is no in-memory cache to consult anyway). + // A missing store file means the initial value. + let store = SecretStore() + return store.hasSecret ? try store.get() : "" + case .counter, .message, .flag: return get(key) } } @@ -363,7 +355,14 @@ public final class StateStore { _ = try readNoteFromDiskIfPresent() case .profile, .profileName: _ = try readProfileFromDiskIfPresent() - case .counter, .message, .flag, .secret: + case .secret: + // Loud failures for the encrypted store: corrupt envelope + // (decodingFailed) and wrong passphrase/key (secretUnlockFailed). + let store = SecretStore() + if store.hasSecret { + _ = try store.get() + } + case .counter, .message, .flag: break } } @@ -405,9 +404,7 @@ public final class StateStore { case .profile: _ = Application.fileState(\.profile).value case .secret: -#if canImport(Security) - _ = Application.secureState(\.secret).value -#endif + break // encrypted store has no Observation surface; polling covers it case .profileName: _ = Application.slice(\.profile, \.name).value } diff --git a/Tests/apsTests/APSTests.swift b/Tests/apsTests/APSTests.swift index 6c2dfa3..56bddcc 100644 --- a/Tests/apsTests/APSTests.swift +++ b/Tests/apsTests/APSTests.swift @@ -76,7 +76,7 @@ final class APSTests: XCTestCase { XCTAssertEqual(DemoKey.flag.storage, "StoredState") XCTAssertEqual(DemoKey.note.storage, "FileState") XCTAssertEqual(DemoKey.profile.storage, "FileState") - XCTAssertEqual(DemoKey.secret.storage, "SecureState") + XCTAssertEqual(DemoKey.secret.storage, "EncryptedFile") XCTAssertEqual(DemoKey.profileName.storage, "Slice") XCTAssertEqual(DemoKey.counter.valueType, "Int") XCTAssertEqual(DemoKey.profile.valueType, "ProfileDocument") @@ -85,9 +85,8 @@ final class APSTests: XCTestCase { XCTAssertEqual(DemoKey.allCases.count, 7) XCTAssertTrue(DemoKey.note.detail.contains("FileState")) XCTAssertTrue(DemoKey.profile.detail.contains("profile.json")) - XCTAssertTrue(DemoKey.secret.detail.contains("Keychain")) + XCTAssertTrue(DemoKey.secret.detail.contains("encrypted file")) XCTAssertTrue(DemoKey.profileName.detail.contains("Slice")) - XCTAssertEqual(APSKeychain.secretAccount, "dev.leif.aps/secret") } @MainActor @@ -166,40 +165,63 @@ final class APSTests: XCTestCase { XCTAssertEqual(onDisk.version, 99) } -#if canImport(Security) @MainActor - func testSecretSecureStateRoundTrip() async throws { - throw XCTSkip("SecureState Keychain tests disabled temporarily") + func testSecretEncryptedStoreRoundTrip() async throws { let store = StateStore() try store.set(.secret, value: "top-secret") XCTAssertEqual(store.get(.secret), "top-secret") - let keychain = Application.dependency(\.keychain) - XCTAssertEqual(keychain.get(APSKeychain.secretAccount), "top-secret") - try store.set(.secret, value: "rotated") XCTAssertEqual(store.get(.secret), "rotated") - XCTAssertEqual(keychain.get(APSKeychain.secretAccount), "rotated") + + let path = FileManager.defaultFileStatePath + XCTAssertTrue(FileManager.default.fileExists( + atPath: URL(fileURLWithPath: path).appendingPathComponent("secret.enc").path + )) } @MainActor - func testSecretResetDeletesKeychainItem() async throws { - throw XCTSkip("SecureState Keychain tests disabled temporarily") + func testSecretResetDeletesStoreFile() async throws { let store = StateStore() try store.set(.secret, value: "ephemeral") - XCTAssertEqual( - Application.dependency(\.keychain).get(APSKeychain.secretAccount), - "ephemeral" - ) + let path = FileManager.defaultFileStatePath + let fileURL = URL(fileURLWithPath: path).appendingPathComponent("secret.enc") + XCTAssertTrue(FileManager.default.fileExists(atPath: fileURL.path)) store.reset(.secret) XCTAssertEqual(store.get(.secret), "") - XCTAssertNil(Application.dependency(\.keychain).get(APSKeychain.secretAccount)) + XCTAssertFalse(FileManager.default.fileExists(atPath: fileURL.path)) + } + + @MainActor + func testSecretKeyFilePermissionsAre0600() async throws { + let path = FileManager.defaultFileStatePath + // Ensure a fresh key file is generated (passphrase mode would skip it). + unsetenv("APS_SECRET_PASSPHRASE") + unsetenv("APS_SECRET_USE_PASSPHRASE") + let store = StateStore() + try store.set(.secret, value: "file-key-secret") + + let keyURL = URL(fileURLWithPath: path).appendingPathComponent("secret.key") + let attributes = try FileManager.default.attributesOfItem(atPath: keyURL.path) + XCTAssertEqual(attributes[.posixPermissions] as? Int, 0o600) + } + + @MainActor + func testSecretStoreCorruptEnvelopeThrowsDecodingFailed() async throws { + let store = StateStore() + try store.set(.secret, value: "ok") + let path = FileManager.defaultFileStatePath + let fileURL = URL(fileURLWithPath: path).appendingPathComponent("secret.enc") + try "garbage{{".write(to: fileURL, atomically: false, encoding: .utf8) + + XCTAssertThrowsError(try StateStore.requireDecodableDiskState(for: .secret)) { error in + XCTAssertEqual(error as? APSError, .decodingFailed) + } } @MainActor func testSecretPersistsAcrossStateStoreInstances() async throws { - throw XCTSkip("SecureState Keychain tests disabled temporarily") let writer = StateStore() try writer.set(.secret, value: "shared-secret") @@ -208,21 +230,22 @@ final class APSTests: XCTestCase { reader.reset(.secret) XCTAssertEqual(StateStore().get(.secret), "") - XCTAssertNil(Application.dependency(\.keychain).get(APSKeychain.secretAccount)) } -#else + +#if !os(Windows) @MainActor - func testSecretSetFailsWithoutKeychain() async { + func testSecretPassphraseRoundTripAndWrongKey() async throws { + setenv("APS_SECRET_PASSPHRASE", "correct-horse", 1) + defer { unsetenv("APS_SECRET_PASSPHRASE") } + let store = StateStore() - do { - try store.set(.secret, value: "nope") - XCTFail("Expected keychainUnavailable on platforms without Security") - } catch let error as APSError { - XCTAssertEqual(error, .keychainUnavailable) - } catch { - XCTFail("Unexpected error: \(error)") + try store.set(.secret, value: "battery-staple") + XCTAssertEqual(store.get(.secret), "battery-staple") + + setenv("APS_SECRET_PASSPHRASE", "wrong", 1) + XCTAssertThrowsError(try StateStore.requireDecodableDiskState(for: .secret)) { error in + XCTAssertEqual(error as? APSError, .secretUnlockFailed) } - XCTAssertEqual(store.get(.secret), "") } #endif @@ -701,17 +724,17 @@ final class APSTests: XCTestCase { XCTAssertEqual(APSError.encodingFailed.code, "encoding_failed") XCTAssertEqual(APSError.decodingFailed.code, "decoding_failed") XCTAssertEqual(APSError.persistenceFailed(key: .note).code, "persistence_failed") - XCTAssertEqual(APSError.keychainUnavailable.code, "keychain_unavailable") + XCTAssertEqual(APSError.secretUnlockFailed.code, "secret_unlock_failed") XCTAssertEqual(APSError.corruptState(key: .note).code, "corrupt_state") XCTAssertEqual(APSError.invalidValue(key: .counter, value: "x").exitCode, 64) XCTAssertEqual(APSError.decodingFailed.exitCode, 65) XCTAssertEqual(APSError.corruptState(key: .note).exitCode, 65) - XCTAssertEqual(APSError.keychainUnavailable.exitCode, 69) + XCTAssertEqual(APSError.secretUnlockFailed.exitCode, 69) XCTAssertEqual(APSError.encodingFailed.exitCode, 70) XCTAssertEqual(APSError.persistenceFailed(key: .note).exitCode, 73) - for error in [APSError.invalidValue(key: .flag, value: "x"), .encodingFailed, .decodingFailed, .persistenceFailed(key: .flag), .keychainUnavailable, .corruptState(key: .profile)] as [APSError] { + for error in [APSError.invalidValue(key: .flag, value: "x"), .encodingFailed, .decodingFailed, .persistenceFailed(key: .flag), .secretUnlockFailed, .corruptState(key: .profile)] as [APSError] { XCTAssertFalse(error.hint.isEmpty, "hint required for \(error.code)") } } diff --git a/docs/windows-readiness.md b/docs/windows-readiness.md index 093292d..500a8e5 100644 --- a/docs/windows-readiness.md +++ b/docs/windows-readiness.md @@ -11,7 +11,7 @@ Auditor: agent:cursor |-------|--------| | No SwiftUI imports | Pass: none in `Sources/` or `Tests/` | | Combine / `@ObservedDependency` gated | Pass: `#if canImport(Combine)` and `#if !os(Linux) && !os(Windows)` (see PR #25) | -| No ungated Security.framework | Pass: SecureState / keychain only under `#if canImport(Security)` | +| No ungated Security.framework | Pass: no Security.framework imports remain; `secret` uses the swift-crypto encrypted store (issue #35) | | No hardcoded `~/Library` paths | Pass: none | | `~/.aps` only as default | Pass: `APSPaths` uses `FileManager.homeDirectoryForCurrentUser` + `.aps`; override via `--state-dir` / `APS_HOME` | | swift-crypto for Windows (#35) | Pass (docs): [apple/swift-crypto](https://github.com/apple/swift-crypto) supports Linux and **ARM64 Windows** via SPM (`Crypto` product). Prefer `from: "4.0.0"` (Swift 6) when #35 lands | @@ -23,7 +23,7 @@ Auditor: agent:cursor | Area | Status | |------|--------| | CI | Self-hosted `[self-hosted, macOS]` (`ci.yml`, `trust.yml`) | -| SecureState / Keychain | Available (`canImport(Security)`); Keychain tests/smoke opt-in | +| Encrypted secret store | Available; swift-crypto runs everywhere | | ObservedDependency / Combine | Full path | | Smoke | `Scripts/smoke.sh` | @@ -32,7 +32,7 @@ Auditor: agent:cursor | Area | Status | |------|--------| | CI | GitHub-hosted `ubuntu-latest` (`linux-smoke.yml`) | -| SecureState | Unavailable; `keychainUnavailable` on set | +| Encrypted secret store | Available; swift-crypto runs on Windows (audit: confirm in CI) | | ObservedDependency | Falls back to `@AppDependency` + polling for `aps stats --watch` | | UserDefaults | `synchronize()` after flag writes (Linux flush) | | Smoke | Same bash script | diff --git a/specs/aps-cli/aps-cli.spec.md b/specs/aps-cli/aps-cli.spec.md index 2eaf0c5..d229c8e 100644 --- a/specs/aps-cli/aps-cli.spec.md +++ b/specs/aps-cli/aps-cli.spec.md @@ -1,6 +1,6 @@ --- module: aps-cli -version: 22 +version: 23 status: active files: - Sources/aps/Aps.swift @@ -8,6 +8,7 @@ files: - Sources/aps/DemoKey.swift - Sources/aps/TTY.swift - Sources/aps/WatchTermination.swift + - Sources/aps/SecretStore.swift db_tables: [] depends_on: - state-store @@ -30,26 +31,27 @@ agents can get, set, watch, dump, list, and reset typed application state. | `name` | ProfileDocument display name field. | | `version` | ProfileDocument integer version field. | | `init` | ProfileDocument memberwise initializer. | -| `APSKeychain` | Well-known Keychain service/account for `secret`. | -| `service` | Reverse-DNS Keychain feature namespace (`dev.leif.aps`). | -| `account` | Keychain account id (`secret`). | -| `secretAccount` | Full account key (`dev.leif.aps/secret`). | | `APSError` | Typed CLI/domain errors. | | `counter` | Int key stored in AppState `State`. | | `message` | String key stored in AppState `State`. | | `flag` | Bool key stored in AppState `StoredState`. | | `note` | String key stored in AppState `FileState`. | | `profile` | ProfileDocument key stored in AppState `FileState`. | -| `secret` | String key stored in AppState `SecureState` (Keychain). | +| `secret` | String key stored in the encrypted-file secret store (`secret.enc`). | +| `SecretStore` | Encrypted-file store: `get` / `set` / `reset` over `secret.enc`. | +| `hasSecret` | True when a store file exists (missing means initial value). | +| `get` | Decrypt the stored secret; loud corrupt/unlock failures. | +| `set` | Encrypt, persist, and read-back verify. | +| `reset` | Delete `secret.enc`, restoring the initial value. | | `profileName` | String Slice over `ProfileDocument.name`. | | `invalidValue` | Value could not parse for the key type. | | `encodingFailed` | UTF-8 JSON encode failure. | | `decodingFailed` | UTF-8 JSON decode failure. | -| `persistenceFailed` | Disk/Keychain-backed key did not persist after write. | -| `keychainUnavailable` | SecureState unavailable without Apple Security. | +| `persistenceFailed` | Disk-backed key did not persist after write. | +| `secretUnlockFailed` | Secret store would not open (wrong passphrase or key). | | `corruptState` | FileState file exists but is undecodable (torn write). | | `corruptStateExitCode` | Exit code 65 (`EX_DATAERR`) for `corruptState`. | -| `storage` | Human storage kind (`State` / `StoredState` / `FileState` / `SecureState` / `Slice`). | +| `storage` | Human storage kind (`State` / `StoredState` / `FileState` / `EncryptedFile` / `Slice`). | | `valueType` | Human value type (`Int` / `String` / `Bool` / `ProfileDocument`). | | `helpSummary` | Tab-separated key/type/storage columns for `keys`. | | `detail` | One-line description for `keys`. | @@ -169,3 +171,4 @@ Exit codes (sysexits-aligned): | 2026-07-19 | CHG-0021-error-contract-exit-code-taxonomy-and-json-error-envelope-issue-31-rebuilt-on: Error contract: exit-code taxonomy and JSON error envelope (issue 31, rebuilt on corruptState main) | | 2026-07-19 | CHG-0022-tty-aware-output-under-the-git-porcelain-rule-issue-33: TTY-aware output under the git porcelain rule (issue 33) | | 2026-07-19 | CHG-0023-watch-signal-handling-and-termination-semantics-issue-34: Watch signal handling and termination semantics (issue 34) | +| 2026-07-19 | CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35: Encrypted-file secret store via swift-crypto (issue 35) | diff --git a/specs/aps-cli/requirements.md b/specs/aps-cli/requirements.md index 48e3965..8dd7af9 100644 --- a/specs/aps-cli/requirements.md +++ b/specs/aps-cli/requirements.md @@ -100,13 +100,7 @@ Acceptance Criteria ### REQ-aps-cli-015 -`secret` SHALL use a well-named Keychain identity and document headless/CI availability. - -Acceptance Criteria -- Keychain account is `dev.leif.aps/secret` (`APSKeychain.secretAccount`). -- `aps reset secret` deletes the Keychain item on macOS. -- README documents macOS Keychain access, Linux unavailability, and headless CI caveats. -- `set secret` on platforms without Security surfaces `keychainUnavailable`. +Superseded by REQ-aps-cli-020 (encrypted-file secret store; issue #35). @@ -149,3 +143,13 @@ Acceptance Criteria - `watch --json` behaves as `--jsonl`; `keys --quiet` prints key names only. - Shell completion scripts (bash/zsh/fish) are documented in README. +### REQ-aps-cli-020 + +The `secret` key SHALL be backed by an encrypted-file secret store under the state root (ephemeral X25519 + HKDF + ChaCha20-Poly1305 via swift-crypto), with zero interactive prompts in key-file mode and passphrase mode via `APS_SECRET_PASSPHRASE`. + +Acceptance Criteria +- `secret` round-trips set/get/reset with ciphertext at rest in `secret.enc`; the key file is mode 0600. +- No Security.framework/Keychain imports; works on macOS and Linux. +- Wrong passphrase fails with `APSError.secretUnlockFailed`; corrupt envelope fails with `APSError.decodingFailed`. +- Passphrase entry is env-var based; an optional TTY getpass prompt exists when `APS_SECRET_USE_PASSPHRASE=1`. + diff --git a/specs/state-store/requirements.md b/specs/state-store/requirements.md index 06ea75e..017b2c1 100644 --- a/specs/state-store/requirements.md +++ b/specs/state-store/requirements.md @@ -72,12 +72,8 @@ Acceptance Criteria ### REQ-state-store-013 -`StateStore` SHALL expose `secret` as Keychain-backed `SecureState` with account `dev.leif.aps/secret`, verify Keychain read-back after set, and delete the item on reset. +Superseded by REQ-aps-cli-020 (encrypted-file secret store via `SecretStore`; issue #35). -Acceptance Criteria -- Round-trip get/set works on macOS. -- `reset(.secret)` leaves get as "" and removes the Keychain item. -- Without Security, `set(.secret, ...)` throws `APSError.keychainUnavailable`. diff --git a/specs/state-store/state-store.spec.md b/specs/state-store/state-store.spec.md index 89cfd1a..a804d87 100644 --- a/specs/state-store/state-store.spec.md +++ b/specs/state-store/state-store.spec.md @@ -1,6 +1,6 @@ --- module: state-store -version: 19 +version: 22 status: active files: - Sources/aps/StateStore.swift @@ -126,3 +126,4 @@ Then keys include message with value "hi" and a timestamp field exists. | 2026-07-18 | CHG-0016-loud-torn-filestate-reads-and-document-multi-writer-semantics-for-issue-38: Loud torn FileState reads and document multi-writer semantics for issue 38 | | 2026-07-18 | CHG-0022-tty-aware-output-git-porcelain-rule-for-issue-33: TTY-aware output under the git porcelain rule for issue 33 | | 2026-07-19 | CHG-0021-tty-aware-output-under-the-git-porcelain-rule-issue-33: TTY-aware output under the git porcelain rule (issue 33) | +| 2026-07-19 | CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35: Encrypted-file secret store via swift-crypto (issue 35) | From 0e35f56795483e3b36f2df97fd0cdf59b60c95a2 Mon Sep 17 00:00:00 2001 From: 0xLeif Date: Sat, 18 Jul 2026 20:57:20 -0600 Subject: [PATCH 02/17] Fix linux-smoke: bump Swift toolchain to 6.1 swift-crypto's transitive swift-asn1 1.7.1 requires tools 6.1; the Linux job pinned 6.0 and failed to resolve. --- .github/workflows/linux-smoke.yml | 2 +- .../verification-attempts.json | 24 ++++++ .../approvals.json | 79 ++++++++++++++++++- .../state.json | 2 +- .../verification-attempts.json | 15 ++++ .../verification.json | 12 +-- 6 files changed, 125 insertions(+), 9 deletions(-) diff --git a/.github/workflows/linux-smoke.yml b/.github/workflows/linux-smoke.yml index 502c848..c94038f 100644 --- a/.github/workflows/linux-smoke.yml +++ b/.github/workflows/linux-smoke.yml @@ -26,7 +26,7 @@ jobs: - name: Install Swift uses: swift-actions/setup-swift@v2 with: - swift-version: "6.0" + swift-version: "6.1" # swift-asn1 (swift-crypto dep) requires tools 6.1+ - name: Swift version run: swift --version diff --git a/.specsync/changes/CHG-0004-ship-aps-0-2-0-agent-ready-json-state-dir-watch-and-profile-filestate/verification-attempts.json b/.specsync/changes/CHG-0004-ship-aps-0-2-0-agent-ready-json-state-dir-watch-and-profile-filestate/verification-attempts.json index 69dd308..1e6591e 100644 --- a/.specsync/changes/CHG-0004-ship-aps-0-2-0-agent-ready-json-state-dir-watch-and-profile-filestate/verification-attempts.json +++ b/.specsync/changes/CHG-0004-ship-aps-0-2-0-agent-ready-json-state-dir-watch-and-profile-filestate/verification-attempts.json @@ -720,6 +720,30 @@ "REQ-state-store-010", "REQ-state-store-011" ] + }, + { + "timestamp": 1784429732, + "commit": "97b784fc4b6925ddc54900d70ab2f0a273ad1eb5", + "contract_digest": "73c000607b3050c54981b4c29532280dd08bf00b7a15c8ec1801e045fa5496c9", + "workspace_digest": "f8fa9433c36fea5ee5f024de58e407f682d1443b439aa2488bb3985eed86e9f4", + "passed": true, + "commands": [ + { + "command": "fledge lanes run verify", + "success": true, + "exit_code": 0 + } + ], + "requirement_ids": [ + "REQ-aps-cli-001", + "REQ-aps-cli-010", + "REQ-aps-cli-011", + "REQ-aps-cli-012", + "REQ-aps-cli-013", + "REQ-state-store-001", + "REQ-state-store-010", + "REQ-state-store-011" + ] } ] } diff --git a/.specsync/changes/CHG-0010-ci-eliminate-the-double-build-for-issue-14/approvals.json b/.specsync/changes/CHG-0010-ci-eliminate-the-double-build-for-issue-14/approvals.json index 9a1c888..3dcb2a5 100644 --- a/.specsync/changes/CHG-0010-ci-eliminate-the-double-build-for-issue-14/approvals.json +++ b/.specsync/changes/CHG-0010-ci-eliminate-the-double-build-for-issue-14/approvals.json @@ -13,7 +13,84 @@ "timestamp": 1784405791, "digest": "561c9a2b468c43660b744c5206c8cf9ac66c1f31e06dba1ffb3ac04ef7a0b32c", "note": "Accept CI single-config jobs." + }, + { + "gate": "acceptance", + "actor": "0xLeif", + "timestamp": 1784429776, + "digest": "17dc2a403576acb33130a7ebb59229d03bd15129523eaaf701fcb047cfb4cb2d", + "note": null } ], - "reopenings": [] + "reopenings": [ + { + "schema_version": 1, + "change_id": "CHG-0010-ci-eliminate-the-double-build-for-issue-14", + "actor": "0xLeif", + "reason": "linux-smoke Swift toolchain bump to 6.1 for swift-asn1; rebinding evidence", + "timestamp": 1784429748, + "from_state": "accepted", + "to_state": "verifying", + "superseded_approval": { + "gate": "acceptance", + "actor": "agent:cursor", + "timestamp": 1784405791, + "digest": "561c9a2b468c43660b744c5206c8cf9ac66c1f31e06dba1ffb3ac04ef7a0b32c", + "note": "Accept CI single-config jobs." + }, + "prior_verification": { + "timestamp": 1784405785, + "commit": "778676002ccdf9bbc6646fd2435644a902f73801", + "contract_digest": "36ad6788d34d58f8bdcf39e67a8f44fdba54ac7f513570c6eaf5e6aace6b68b7", + "workspace_digest": "f70d16789fed3ce53693f9f72219ffa4bd8888b29c21c42ddd17e6c810c38c5c", + "acceptance_input_digest": "105f5ab3dbdb0beb0c2362f210775a257bf580d6fcf814e8153a815d4474218b", + "acceptance_manifest": { + "schema_version": 1, + "entries": [ + { + "path": ".github/workflows/ci.yml", + "kind": "file", + "mode": 33188, + "payload_digest": "a902f7c6785bb487278bfb39c95d593a45a8695f5245a9e5b9909d810410e6a8", + "entry_digest": "00a1986a1f0e06e1cbd4d3c3e933b240c5cb457a554e86515ba3db8241f7a263", + "owners": [ + "@exact:delivery" + ] + }, + { + "path": ".github/workflows/linux-smoke.yml", + "kind": "file", + "mode": 33188, + "payload_digest": "7a7f76bbc068b14e9945a3add8c1f1d812362d810db6af6c4c9b60959d54f5c3", + "entry_digest": "db8fe6d1745786e008c9b0f6d2631cbf6a9e094763f305cfe07bfbc4444b0535", + "owners": [ + "@exact:delivery" + ] + }, + { + "path": ".specsync/change-sequence.json", + "kind": "file", + "mode": 33188, + "payload_digest": "4596b5346d690154141e9faa364f8c2934abde2e31083f80896552576b76762b", + "entry_digest": "f38e6dfc1754c4aa014cc51b7e54e22ee0b01ffe2656eefe2a413bd4111c9a79", + "owners": [ + "@exact:delivery" + ] + } + ] + }, + "passed": true, + "commands": [ + { + "command": "fledge lanes run verify", + "success": true, + "exit_code": 0 + } + ], + "requirement_ids": [] + }, + "stale_acceptance_input_digest": "105f5ab3dbdb0beb0c2362f210775a257bf580d6fcf814e8153a815d4474218b", + "current_acceptance_input_digest": "57c257129c29cd42ee821db7216cd444d0762a2e49f3b37cb406393080bd4c37" + } + ] } diff --git a/.specsync/changes/CHG-0010-ci-eliminate-the-double-build-for-issue-14/state.json b/.specsync/changes/CHG-0010-ci-eliminate-the-double-build-for-issue-14/state.json index a9b57e8..09bc133 100644 --- a/.specsync/changes/CHG-0010-ci-eliminate-the-double-build-for-issue-14/state.json +++ b/.specsync/changes/CHG-0010-ci-eliminate-the-double-build-for-issue-14/state.json @@ -9,7 +9,7 @@ "canonical_applied": true, "base_commit": "778676002ccdf9bbc6646fd2435644a902f73801", "created_at": 1784405770, - "updated_at": 1784405791, + "updated_at": 1784429776, "affected_specs": [], "affected_paths": [ ".github/workflows/ci.yml", diff --git a/.specsync/changes/CHG-0010-ci-eliminate-the-double-build-for-issue-14/verification-attempts.json b/.specsync/changes/CHG-0010-ci-eliminate-the-double-build-for-issue-14/verification-attempts.json index 9bf1a53..eca3113 100644 --- a/.specsync/changes/CHG-0010-ci-eliminate-the-double-build-for-issue-14/verification-attempts.json +++ b/.specsync/changes/CHG-0010-ci-eliminate-the-double-build-for-issue-14/verification-attempts.json @@ -15,6 +15,21 @@ } ], "requirement_ids": [] + }, + { + "timestamp": 1784429767, + "commit": "97b784fc4b6925ddc54900d70ab2f0a273ad1eb5", + "contract_digest": "36ad6788d34d58f8bdcf39e67a8f44fdba54ac7f513570c6eaf5e6aace6b68b7", + "workspace_digest": "f8fa9433c36fea5ee5f024de58e407f682d1443b439aa2488bb3985eed86e9f4", + "passed": true, + "commands": [ + { + "command": "fledge lanes run verify", + "success": true, + "exit_code": 0 + } + ], + "requirement_ids": [] } ] } diff --git a/.specsync/changes/CHG-0010-ci-eliminate-the-double-build-for-issue-14/verification.json b/.specsync/changes/CHG-0010-ci-eliminate-the-double-build-for-issue-14/verification.json index ebfa327..8cdeb9f 100644 --- a/.specsync/changes/CHG-0010-ci-eliminate-the-double-build-for-issue-14/verification.json +++ b/.specsync/changes/CHG-0010-ci-eliminate-the-double-build-for-issue-14/verification.json @@ -1,9 +1,9 @@ { - "timestamp": 1784405785, - "commit": "778676002ccdf9bbc6646fd2435644a902f73801", + "timestamp": 1784429767, + "commit": "97b784fc4b6925ddc54900d70ab2f0a273ad1eb5", "contract_digest": "36ad6788d34d58f8bdcf39e67a8f44fdba54ac7f513570c6eaf5e6aace6b68b7", - "workspace_digest": "f70d16789fed3ce53693f9f72219ffa4bd8888b29c21c42ddd17e6c810c38c5c", - "acceptance_input_digest": "105f5ab3dbdb0beb0c2362f210775a257bf580d6fcf814e8153a815d4474218b", + "workspace_digest": "f8fa9433c36fea5ee5f024de58e407f682d1443b439aa2488bb3985eed86e9f4", + "acceptance_input_digest": "57c257129c29cd42ee821db7216cd444d0762a2e49f3b37cb406393080bd4c37", "acceptance_manifest": { "schema_version": 1, "entries": [ @@ -21,8 +21,8 @@ "path": ".github/workflows/linux-smoke.yml", "kind": "file", "mode": 33188, - "payload_digest": "7a7f76bbc068b14e9945a3add8c1f1d812362d810db6af6c4c9b60959d54f5c3", - "entry_digest": "db8fe6d1745786e008c9b0f6d2631cbf6a9e094763f305cfe07bfbc4444b0535", + "payload_digest": "38d34c80465d01e5693a3ffb7335075d481062e0b0ad3f814bc344748ca85b25", + "entry_digest": "7e086be5df0373452d73c256064f7629a2f54b423dd986567ef3f8a0972d2e38", "owners": [ "@exact:delivery" ] From b3b5d2b63b6a6598eec3ccbf4b903bdbfbc8f2c4 Mon Sep 17 00:00:00 2001 From: 0xLeif Date: Sat, 18 Jul 2026 21:12:06 -0600 Subject: [PATCH 03/17] linux-smoke: align toolchain to 6.3.1 6.1.3's libswiftObservation has a linker bug (undefined swift::threading::fatal); 6.3.1 matches the windows-smoke toolchain. --- .github/workflows/linux-smoke.yml | 2 +- .../verification-attempts.json | 24 ++++++ .../approvals.json | 76 +++++++++++++++++++ .../state.json | 2 +- .../verification-attempts.json | 15 ++++ .../verification.json | 12 +-- 6 files changed, 123 insertions(+), 8 deletions(-) diff --git a/.github/workflows/linux-smoke.yml b/.github/workflows/linux-smoke.yml index c94038f..f917948 100644 --- a/.github/workflows/linux-smoke.yml +++ b/.github/workflows/linux-smoke.yml @@ -26,7 +26,7 @@ jobs: - name: Install Swift uses: swift-actions/setup-swift@v2 with: - swift-version: "6.1" # swift-asn1 (swift-crypto dep) requires tools 6.1+ + swift-version: "6.3.1" # match windows toolchain; 6.1.3 has an Observation linker bug (swift::threading::fatal) - name: Swift version run: swift --version diff --git a/.specsync/changes/CHG-0004-ship-aps-0-2-0-agent-ready-json-state-dir-watch-and-profile-filestate/verification-attempts.json b/.specsync/changes/CHG-0004-ship-aps-0-2-0-agent-ready-json-state-dir-watch-and-profile-filestate/verification-attempts.json index 1e6591e..ddcb947 100644 --- a/.specsync/changes/CHG-0004-ship-aps-0-2-0-agent-ready-json-state-dir-watch-and-profile-filestate/verification-attempts.json +++ b/.specsync/changes/CHG-0004-ship-aps-0-2-0-agent-ready-json-state-dir-watch-and-profile-filestate/verification-attempts.json @@ -744,6 +744,30 @@ "REQ-state-store-010", "REQ-state-store-011" ] + }, + { + "timestamp": 1784430523, + "commit": "c1dc563bfed6bcbbfa594fbf5a811a48294a54a3", + "contract_digest": "73c000607b3050c54981b4c29532280dd08bf00b7a15c8ec1801e045fa5496c9", + "workspace_digest": "e17d700c1eeb45a3dd219b0a15a35be24ba1d13af121a6036f18c25108111995", + "passed": true, + "commands": [ + { + "command": "fledge lanes run verify", + "success": true, + "exit_code": 0 + } + ], + "requirement_ids": [ + "REQ-aps-cli-001", + "REQ-aps-cli-010", + "REQ-aps-cli-011", + "REQ-aps-cli-012", + "REQ-aps-cli-013", + "REQ-state-store-001", + "REQ-state-store-010", + "REQ-state-store-011" + ] } ] } diff --git a/.specsync/changes/CHG-0010-ci-eliminate-the-double-build-for-issue-14/approvals.json b/.specsync/changes/CHG-0010-ci-eliminate-the-double-build-for-issue-14/approvals.json index 3dcb2a5..8ae5390 100644 --- a/.specsync/changes/CHG-0010-ci-eliminate-the-double-build-for-issue-14/approvals.json +++ b/.specsync/changes/CHG-0010-ci-eliminate-the-double-build-for-issue-14/approvals.json @@ -20,6 +20,13 @@ "timestamp": 1784429776, "digest": "17dc2a403576acb33130a7ebb59229d03bd15129523eaaf701fcb047cfb4cb2d", "note": null + }, + { + "gate": "acceptance", + "actor": "0xLeif", + "timestamp": 1784430617, + "digest": "50f5e85ee350c547a32d9c696200714a9234fc4ce911350638996937431f0606", + "note": null } ], "reopenings": [ @@ -91,6 +98,75 @@ }, "stale_acceptance_input_digest": "105f5ab3dbdb0beb0c2362f210775a257bf580d6fcf814e8153a815d4474218b", "current_acceptance_input_digest": "57c257129c29cd42ee821db7216cd444d0762a2e49f3b37cb406393080bd4c37" + }, + { + "schema_version": 1, + "change_id": "CHG-0010-ci-eliminate-the-double-build-for-issue-14", + "actor": "0xLeif", + "reason": "linux-smoke toolchain aligned to 6.3.1; rebinding evidence", + "timestamp": 1784430568, + "from_state": "accepted", + "to_state": "verifying", + "superseded_approval": { + "gate": "acceptance", + "actor": "0xLeif", + "timestamp": 1784429776, + "digest": "17dc2a403576acb33130a7ebb59229d03bd15129523eaaf701fcb047cfb4cb2d", + "note": null + }, + "prior_verification": { + "timestamp": 1784429767, + "commit": "97b784fc4b6925ddc54900d70ab2f0a273ad1eb5", + "contract_digest": "36ad6788d34d58f8bdcf39e67a8f44fdba54ac7f513570c6eaf5e6aace6b68b7", + "workspace_digest": "f8fa9433c36fea5ee5f024de58e407f682d1443b439aa2488bb3985eed86e9f4", + "acceptance_input_digest": "57c257129c29cd42ee821db7216cd444d0762a2e49f3b37cb406393080bd4c37", + "acceptance_manifest": { + "schema_version": 1, + "entries": [ + { + "path": ".github/workflows/ci.yml", + "kind": "file", + "mode": 33188, + "payload_digest": "a902f7c6785bb487278bfb39c95d593a45a8695f5245a9e5b9909d810410e6a8", + "entry_digest": "00a1986a1f0e06e1cbd4d3c3e933b240c5cb457a554e86515ba3db8241f7a263", + "owners": [ + "@exact:delivery" + ] + }, + { + "path": ".github/workflows/linux-smoke.yml", + "kind": "file", + "mode": 33188, + "payload_digest": "38d34c80465d01e5693a3ffb7335075d481062e0b0ad3f814bc344748ca85b25", + "entry_digest": "7e086be5df0373452d73c256064f7629a2f54b423dd986567ef3f8a0972d2e38", + "owners": [ + "@exact:delivery" + ] + }, + { + "path": ".specsync/change-sequence.json", + "kind": "file", + "mode": 33188, + "payload_digest": "4596b5346d690154141e9faa364f8c2934abde2e31083f80896552576b76762b", + "entry_digest": "f38e6dfc1754c4aa014cc51b7e54e22ee0b01ffe2656eefe2a413bd4111c9a79", + "owners": [ + "@exact:delivery" + ] + } + ] + }, + "passed": true, + "commands": [ + { + "command": "fledge lanes run verify", + "success": true, + "exit_code": 0 + } + ], + "requirement_ids": [] + }, + "stale_acceptance_input_digest": "57c257129c29cd42ee821db7216cd444d0762a2e49f3b37cb406393080bd4c37", + "current_acceptance_input_digest": "8054c9cd918710d668db8048f7126313662470b52b299c572057ec0a2d90b0d0" } ] } diff --git a/.specsync/changes/CHG-0010-ci-eliminate-the-double-build-for-issue-14/state.json b/.specsync/changes/CHG-0010-ci-eliminate-the-double-build-for-issue-14/state.json index 09bc133..08b1295 100644 --- a/.specsync/changes/CHG-0010-ci-eliminate-the-double-build-for-issue-14/state.json +++ b/.specsync/changes/CHG-0010-ci-eliminate-the-double-build-for-issue-14/state.json @@ -9,7 +9,7 @@ "canonical_applied": true, "base_commit": "778676002ccdf9bbc6646fd2435644a902f73801", "created_at": 1784405770, - "updated_at": 1784429776, + "updated_at": 1784430617, "affected_specs": [], "affected_paths": [ ".github/workflows/ci.yml", diff --git a/.specsync/changes/CHG-0010-ci-eliminate-the-double-build-for-issue-14/verification-attempts.json b/.specsync/changes/CHG-0010-ci-eliminate-the-double-build-for-issue-14/verification-attempts.json index eca3113..a3eb3fa 100644 --- a/.specsync/changes/CHG-0010-ci-eliminate-the-double-build-for-issue-14/verification-attempts.json +++ b/.specsync/changes/CHG-0010-ci-eliminate-the-double-build-for-issue-14/verification-attempts.json @@ -30,6 +30,21 @@ } ], "requirement_ids": [] + }, + { + "timestamp": 1784430593, + "commit": "c1dc563bfed6bcbbfa594fbf5a811a48294a54a3", + "contract_digest": "36ad6788d34d58f8bdcf39e67a8f44fdba54ac7f513570c6eaf5e6aace6b68b7", + "workspace_digest": "e17d700c1eeb45a3dd219b0a15a35be24ba1d13af121a6036f18c25108111995", + "passed": true, + "commands": [ + { + "command": "fledge lanes run verify", + "success": true, + "exit_code": 0 + } + ], + "requirement_ids": [] } ] } diff --git a/.specsync/changes/CHG-0010-ci-eliminate-the-double-build-for-issue-14/verification.json b/.specsync/changes/CHG-0010-ci-eliminate-the-double-build-for-issue-14/verification.json index 8cdeb9f..cb9b1de 100644 --- a/.specsync/changes/CHG-0010-ci-eliminate-the-double-build-for-issue-14/verification.json +++ b/.specsync/changes/CHG-0010-ci-eliminate-the-double-build-for-issue-14/verification.json @@ -1,9 +1,9 @@ { - "timestamp": 1784429767, - "commit": "97b784fc4b6925ddc54900d70ab2f0a273ad1eb5", + "timestamp": 1784430593, + "commit": "c1dc563bfed6bcbbfa594fbf5a811a48294a54a3", "contract_digest": "36ad6788d34d58f8bdcf39e67a8f44fdba54ac7f513570c6eaf5e6aace6b68b7", - "workspace_digest": "f8fa9433c36fea5ee5f024de58e407f682d1443b439aa2488bb3985eed86e9f4", - "acceptance_input_digest": "57c257129c29cd42ee821db7216cd444d0762a2e49f3b37cb406393080bd4c37", + "workspace_digest": "e17d700c1eeb45a3dd219b0a15a35be24ba1d13af121a6036f18c25108111995", + "acceptance_input_digest": "8054c9cd918710d668db8048f7126313662470b52b299c572057ec0a2d90b0d0", "acceptance_manifest": { "schema_version": 1, "entries": [ @@ -21,8 +21,8 @@ "path": ".github/workflows/linux-smoke.yml", "kind": "file", "mode": 33188, - "payload_digest": "38d34c80465d01e5693a3ffb7335075d481062e0b0ad3f814bc344748ca85b25", - "entry_digest": "7e086be5df0373452d73c256064f7629a2f54b423dd986567ef3f8a0972d2e38", + "payload_digest": "e5164a13b7836d5e0e8c05c6ed58649b6e91228b182c22407c0cbf36325ae738", + "entry_digest": "d4e80b4c9ff6fa711e588857def7c9b5961a9c2db0dd9e8748825269aea58d70", "owners": [ "@exact:delivery" ] From aa081893470955613e6154b55e354d74e9bcffb3 Mon Sep 17 00:00:00 2001 From: 0xLeif Date: Sat, 18 Jul 2026 21:17:21 -0600 Subject: [PATCH 04/17] linux-smoke: use Swift 6.2 (6.3.1 unavailable on setup-swift for Ubuntu) --- .github/workflows/linux-smoke.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/linux-smoke.yml b/.github/workflows/linux-smoke.yml index f917948..6296b47 100644 --- a/.github/workflows/linux-smoke.yml +++ b/.github/workflows/linux-smoke.yml @@ -26,7 +26,7 @@ jobs: - name: Install Swift uses: swift-actions/setup-swift@v2 with: - swift-version: "6.3.1" # match windows toolchain; 6.1.3 has an Observation linker bug (swift::threading::fatal) + swift-version: "6.2" # newest available on setup-swift for Ubuntu; avoids the 6.1.3 Observation linker bug - name: Swift version run: swift --version From 7265542389d1b6299ebc2c1e8a5c1ac750682b5a Mon Sep 17 00:00:00 2001 From: 0xLeif Date: Sat, 18 Jul 2026 21:28:57 -0600 Subject: [PATCH 05/17] Fix linux-smoke properly: cap swift-asn1 below 1.7, keep Swift 6.0 swift-asn1 1.7+ requires tools 6.1, and the 6.1/6.2 Linux toolchains fail to link Observation (swift::threading::fatal). swift-asn1 1.6.0 keeps the whole graph 6.0-compatible, matching the historically green 6.0 job. --- .github/workflows/linux-smoke.yml | 2 +- .../verification-attempts.json | 24 ++ .../approvals.json | 76 +++++ .../state.json | 2 +- .../verification-attempts.json | 15 + .../verification.json | 12 +- .../approvals.json | 282 +++++++++++++++++- .../state.json | 2 +- .../verification-attempts.json | 18 ++ .../verification.json | 16 +- Package.resolved | 6 +- Package.swift | 5 +- 12 files changed, 438 insertions(+), 22 deletions(-) diff --git a/.github/workflows/linux-smoke.yml b/.github/workflows/linux-smoke.yml index 6296b47..45681e4 100644 --- a/.github/workflows/linux-smoke.yml +++ b/.github/workflows/linux-smoke.yml @@ -26,7 +26,7 @@ jobs: - name: Install Swift uses: swift-actions/setup-swift@v2 with: - swift-version: "6.2" # newest available on setup-swift for Ubuntu; avoids the 6.1.3 Observation linker bug + swift-version: "6.0" # 6.1/6.2 Linux toolchains have an Observation linker bug; swift-asn1 is capped in Package.swift to stay 6.0-compatible - name: Swift version run: swift --version diff --git a/.specsync/changes/CHG-0004-ship-aps-0-2-0-agent-ready-json-state-dir-watch-and-profile-filestate/verification-attempts.json b/.specsync/changes/CHG-0004-ship-aps-0-2-0-agent-ready-json-state-dir-watch-and-profile-filestate/verification-attempts.json index ddcb947..3373e74 100644 --- a/.specsync/changes/CHG-0004-ship-aps-0-2-0-agent-ready-json-state-dir-watch-and-profile-filestate/verification-attempts.json +++ b/.specsync/changes/CHG-0004-ship-aps-0-2-0-agent-ready-json-state-dir-watch-and-profile-filestate/verification-attempts.json @@ -768,6 +768,30 @@ "REQ-state-store-010", "REQ-state-store-011" ] + }, + { + "timestamp": 1784431619, + "commit": "408aa1da3ab880836e3aa7eb1348dd5f3cce3ca9", + "contract_digest": "73c000607b3050c54981b4c29532280dd08bf00b7a15c8ec1801e045fa5496c9", + "workspace_digest": "022da05d019d831880b3d1ba8341cece917151c2a61a1a7a4b153e47c7f023d8", + "passed": true, + "commands": [ + { + "command": "fledge lanes run verify", + "success": true, + "exit_code": 0 + } + ], + "requirement_ids": [ + "REQ-aps-cli-001", + "REQ-aps-cli-010", + "REQ-aps-cli-011", + "REQ-aps-cli-012", + "REQ-aps-cli-013", + "REQ-state-store-001", + "REQ-state-store-010", + "REQ-state-store-011" + ] } ] } diff --git a/.specsync/changes/CHG-0010-ci-eliminate-the-double-build-for-issue-14/approvals.json b/.specsync/changes/CHG-0010-ci-eliminate-the-double-build-for-issue-14/approvals.json index 8ae5390..ac2d241 100644 --- a/.specsync/changes/CHG-0010-ci-eliminate-the-double-build-for-issue-14/approvals.json +++ b/.specsync/changes/CHG-0010-ci-eliminate-the-double-build-for-issue-14/approvals.json @@ -27,6 +27,13 @@ "timestamp": 1784430617, "digest": "50f5e85ee350c547a32d9c696200714a9234fc4ce911350638996937431f0606", "note": null + }, + { + "gate": "acceptance", + "actor": "0xLeif", + "timestamp": 1784431655, + "digest": "c1b8f8a16ae79535301d1a77bb0f602c0869b0bd37984c6a56349771694c194b", + "note": null } ], "reopenings": [ @@ -167,6 +174,75 @@ }, "stale_acceptance_input_digest": "57c257129c29cd42ee821db7216cd444d0762a2e49f3b37cb406393080bd4c37", "current_acceptance_input_digest": "8054c9cd918710d668db8048f7126313662470b52b299c572057ec0a2d90b0d0" + }, + { + "schema_version": 1, + "change_id": "CHG-0010-ci-eliminate-the-double-build-for-issue-14", + "actor": "0xLeif", + "reason": "swift-asn1 capped and linux toolchain reverted to 6.0; rebinding evidence", + "timestamp": 1784431634, + "from_state": "accepted", + "to_state": "verifying", + "superseded_approval": { + "gate": "acceptance", + "actor": "0xLeif", + "timestamp": 1784430617, + "digest": "50f5e85ee350c547a32d9c696200714a9234fc4ce911350638996937431f0606", + "note": null + }, + "prior_verification": { + "timestamp": 1784430593, + "commit": "c1dc563bfed6bcbbfa594fbf5a811a48294a54a3", + "contract_digest": "36ad6788d34d58f8bdcf39e67a8f44fdba54ac7f513570c6eaf5e6aace6b68b7", + "workspace_digest": "e17d700c1eeb45a3dd219b0a15a35be24ba1d13af121a6036f18c25108111995", + "acceptance_input_digest": "8054c9cd918710d668db8048f7126313662470b52b299c572057ec0a2d90b0d0", + "acceptance_manifest": { + "schema_version": 1, + "entries": [ + { + "path": ".github/workflows/ci.yml", + "kind": "file", + "mode": 33188, + "payload_digest": "a902f7c6785bb487278bfb39c95d593a45a8695f5245a9e5b9909d810410e6a8", + "entry_digest": "00a1986a1f0e06e1cbd4d3c3e933b240c5cb457a554e86515ba3db8241f7a263", + "owners": [ + "@exact:delivery" + ] + }, + { + "path": ".github/workflows/linux-smoke.yml", + "kind": "file", + "mode": 33188, + "payload_digest": "e5164a13b7836d5e0e8c05c6ed58649b6e91228b182c22407c0cbf36325ae738", + "entry_digest": "d4e80b4c9ff6fa711e588857def7c9b5961a9c2db0dd9e8748825269aea58d70", + "owners": [ + "@exact:delivery" + ] + }, + { + "path": ".specsync/change-sequence.json", + "kind": "file", + "mode": 33188, + "payload_digest": "4596b5346d690154141e9faa364f8c2934abde2e31083f80896552576b76762b", + "entry_digest": "f38e6dfc1754c4aa014cc51b7e54e22ee0b01ffe2656eefe2a413bd4111c9a79", + "owners": [ + "@exact:delivery" + ] + } + ] + }, + "passed": true, + "commands": [ + { + "command": "fledge lanes run verify", + "success": true, + "exit_code": 0 + } + ], + "requirement_ids": [] + }, + "stale_acceptance_input_digest": "8054c9cd918710d668db8048f7126313662470b52b299c572057ec0a2d90b0d0", + "current_acceptance_input_digest": "721ce626d1e5fa2ed852bbaeb13c5138d381c9bba89821dd81a09c239fa34678" } ] } diff --git a/.specsync/changes/CHG-0010-ci-eliminate-the-double-build-for-issue-14/state.json b/.specsync/changes/CHG-0010-ci-eliminate-the-double-build-for-issue-14/state.json index 08b1295..7fbeee6 100644 --- a/.specsync/changes/CHG-0010-ci-eliminate-the-double-build-for-issue-14/state.json +++ b/.specsync/changes/CHG-0010-ci-eliminate-the-double-build-for-issue-14/state.json @@ -9,7 +9,7 @@ "canonical_applied": true, "base_commit": "778676002ccdf9bbc6646fd2435644a902f73801", "created_at": 1784405770, - "updated_at": 1784430617, + "updated_at": 1784431655, "affected_specs": [], "affected_paths": [ ".github/workflows/ci.yml", diff --git a/.specsync/changes/CHG-0010-ci-eliminate-the-double-build-for-issue-14/verification-attempts.json b/.specsync/changes/CHG-0010-ci-eliminate-the-double-build-for-issue-14/verification-attempts.json index a3eb3fa..c5aab8b 100644 --- a/.specsync/changes/CHG-0010-ci-eliminate-the-double-build-for-issue-14/verification-attempts.json +++ b/.specsync/changes/CHG-0010-ci-eliminate-the-double-build-for-issue-14/verification-attempts.json @@ -45,6 +45,21 @@ } ], "requirement_ids": [] + }, + { + "timestamp": 1784431647, + "commit": "408aa1da3ab880836e3aa7eb1348dd5f3cce3ca9", + "contract_digest": "36ad6788d34d58f8bdcf39e67a8f44fdba54ac7f513570c6eaf5e6aace6b68b7", + "workspace_digest": "022da05d019d831880b3d1ba8341cece917151c2a61a1a7a4b153e47c7f023d8", + "passed": true, + "commands": [ + { + "command": "fledge lanes run verify", + "success": true, + "exit_code": 0 + } + ], + "requirement_ids": [] } ] } diff --git a/.specsync/changes/CHG-0010-ci-eliminate-the-double-build-for-issue-14/verification.json b/.specsync/changes/CHG-0010-ci-eliminate-the-double-build-for-issue-14/verification.json index cb9b1de..f73d82a 100644 --- a/.specsync/changes/CHG-0010-ci-eliminate-the-double-build-for-issue-14/verification.json +++ b/.specsync/changes/CHG-0010-ci-eliminate-the-double-build-for-issue-14/verification.json @@ -1,9 +1,9 @@ { - "timestamp": 1784430593, - "commit": "c1dc563bfed6bcbbfa594fbf5a811a48294a54a3", + "timestamp": 1784431647, + "commit": "408aa1da3ab880836e3aa7eb1348dd5f3cce3ca9", "contract_digest": "36ad6788d34d58f8bdcf39e67a8f44fdba54ac7f513570c6eaf5e6aace6b68b7", - "workspace_digest": "e17d700c1eeb45a3dd219b0a15a35be24ba1d13af121a6036f18c25108111995", - "acceptance_input_digest": "8054c9cd918710d668db8048f7126313662470b52b299c572057ec0a2d90b0d0", + "workspace_digest": "022da05d019d831880b3d1ba8341cece917151c2a61a1a7a4b153e47c7f023d8", + "acceptance_input_digest": "721ce626d1e5fa2ed852bbaeb13c5138d381c9bba89821dd81a09c239fa34678", "acceptance_manifest": { "schema_version": 1, "entries": [ @@ -21,8 +21,8 @@ "path": ".github/workflows/linux-smoke.yml", "kind": "file", "mode": 33188, - "payload_digest": "e5164a13b7836d5e0e8c05c6ed58649b6e91228b182c22407c0cbf36325ae738", - "entry_digest": "d4e80b4c9ff6fa711e588857def7c9b5961a9c2db0dd9e8748825269aea58d70", + "payload_digest": "92de55b091cf658b96d13ecbab10410b359aacc948eda46efec1c71b4335d204", + "entry_digest": "d07d9469e405d4764452c1dff3c0d2f0f12de9bd873e82b60d9c8c534322a160", "owners": [ "@exact:delivery" ] diff --git a/.specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/approvals.json b/.specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/approvals.json index bcef8b0..d94bef2 100644 --- a/.specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/approvals.json +++ b/.specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/approvals.json @@ -13,7 +13,287 @@ "timestamp": 1784427376, "digest": "3d67077ab2a07e353bc39ae80ac266a16b556db7a242601a9178111d94706cc5", "note": null + }, + { + "gate": "acceptance", + "actor": "0xLeif", + "timestamp": 1784431683, + "digest": "53c654192a50e642795ea9b0813e50d67633a820168961b068daab91e107c8bd", + "note": null } ], - "reopenings": [] + "reopenings": [ + { + "schema_version": 1, + "change_id": "CHG-0021-encrypted-file-secret-store-via-swift-crypto-issue-35", + "actor": "0xLeif", + "reason": "swift-asn1 capped and linux toolchain reverted to 6.0; rebinding evidence", + "timestamp": 1784431662, + "from_state": "accepted", + "to_state": "verifying", + "superseded_approval": { + "gate": "acceptance", + "actor": "0xLeif", + "timestamp": 1784427376, + "digest": "3d67077ab2a07e353bc39ae80ac266a16b556db7a242601a9178111d94706cc5", + "note": null + }, + "prior_verification": { + "timestamp": 1784427370, + "commit": "c4ecd60b2a46bca4780f1b4af79a1d495d646287", + "contract_digest": "827ed26145641b8b0d33b4e64e3404a86c75fa2f53393feaeead8250d493964d", + "workspace_digest": "8fa57eaecb8655afb8788e481cb5ea90ade16f2ea9979f71e9a13d66b44e14c1", + "acceptance_input_digest": "11de5fb1ae0313091894fb39437ec2a85ba7a434f61bc48cdb7814894aa55ae6", + "acceptance_manifest": { + "schema_version": 1, + "entries": [ + { + "path": ".specsync/change-sequence.json", + "kind": "file", + "mode": 33188, + "payload_digest": "59a0e39b3046172391fffe9fade542bec84123a2ed01006eb3d2c8eb983b3281", + "entry_digest": "253eb565ab4faed6063db30553febe5c53bcac9fff6e7da4908bfd654abcc0ad", + "owners": [ + "@exact:delivery" + ] + }, + { + "path": "GOAL.md", + "kind": "file", + "mode": 33188, + "payload_digest": "1f202b1275871514d91e71bf71089add50fd54b7b6a01e4c2758f37ea7d7de13", + "entry_digest": "063f926655d07fb9d5f645ac5f092920c81ab092aaee19106d431da7b0a96820", + "owners": [ + "@exact:delivery" + ] + }, + { + "path": "Package.resolved", + "kind": "file", + "mode": 33188, + "payload_digest": "1da38ca760f1067d66c2055b0e6356376643a2747e7b0b782c441e82a1412df3", + "entry_digest": "16e2671c61546e6acf924975b46d72191cf49aa3bb3fa50af98c26928bd42e37", + "owners": [ + "@exact:delivery" + ] + }, + { + "path": "Package.swift", + "kind": "file", + "mode": 33188, + "payload_digest": "fa5c94630e24811d9fb6ba5c3670cbfd608c227ca569d20a6f9bf86e26e110a6", + "entry_digest": "300ea3cc8908fc5f97a879f503b249d389547d01e089a41b9dee33f259e777a6", + "owners": [ + "@exact:delivery" + ] + }, + { + "path": "README.md", + "kind": "file", + "mode": 33188, + "payload_digest": "6fef4366c759779d0eb2514b5c0ed919f68252a057bee450ff43d38f4e3ec0ea", + "entry_digest": "f0caa4bb27ab04a23cd111e69283fcc9460e82c91a47816b8e73eb167f159644", + "owners": [ + "@exact:delivery" + ] + }, + { + "path": "Scripts/smoke.sh", + "kind": "file", + "mode": 33261, + "payload_digest": "7bbfe819c7a6b60ea891ab711ed314ad1ad53111920d17bc14499ea018cb7373", + "entry_digest": "4868e08df80f240a34d4b264f0ef1fb0f5201b4ac09600c11924aac3a38a4874", + "owners": [ + "@exact:delivery" + ] + }, + { + "path": "Sources/aps/DemoKey.swift", + "kind": "file", + "mode": 33188, + "payload_digest": "f361daf0e91573e911a97c6d713783fbdea923a397e83b333efde52b2c11b08c", + "entry_digest": "5df4f7ba4efcd3ac515674c67789235c0ae978f96aa013bc8f3fb7527cdbf4bf", + "owners": [ + "aps-cli" + ] + }, + { + "path": "Sources/aps/DemoState.swift", + "kind": "file", + "mode": 33188, + "payload_digest": "52511df78ff00e335d6154357fb927cb5761cf34fdec2d1c34b07945210eae8c", + "entry_digest": "c131e1cc18c9ddd59fd8c07bb7aa937ea5eacb0abc182811eb1ca09d7a75412e", + "owners": [ + "state-store" + ] + }, + { + "path": "Sources/aps/SecretStore.swift", + "kind": "file", + "mode": 33188, + "payload_digest": "f5247a42cdf94aae7893a24f81fc4608809f9d5f4fd9afb37f4b1e8d99bd7d6a", + "entry_digest": "2f7b32a90e4c70b1b9062201ffc935a66cb1fdfa060b5810d5b32995f916c429", + "owners": [ + "aps-cli" + ] + }, + { + "path": "Sources/aps/StateStore.swift", + "kind": "file", + "mode": 33188, + "payload_digest": "d5f0995ca8c1bbae4489d305de16d5264a486534a4ebdb16de0c9a9828e6c10d", + "entry_digest": "26bf7a564b76388fe42804bd641855168fc03dbd9503bc112b244de677f46c5e", + "owners": [ + "state-store" + ] + }, + { + "path": "Tests/apsTests/APSTests.swift", + "kind": "file", + "mode": 33188, + "payload_digest": "085adc68eb6c47c04b30d22df1bf8d62265593ce46beaccc16ff5dbbb00b7821", + "entry_digest": "330affb934181c0cde7bc76bdaf26c050583b473c9227ff0546d6049edb283f3", + "owners": [ + "@exact:test" + ] + }, + { + "path": "docs/windows-readiness.md", + "kind": "file", + "mode": 33188, + "payload_digest": "a807cbb47e76fb39f27b6136315147be654ae37b044b6f7d73e9dc1fed9b8176", + "entry_digest": "f8614191469e65f306d82023c07dea6c0e95a804380eef9c5d2cf981ed188fb3", + "owners": [ + "@exact:delivery" + ] + }, + { + "path": "specs", + "kind": "non_file", + "mode": 0, + "payload_digest": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "entry_digest": "d654cba9ce3c5b8a9c72964ac1f99b8ae3d9a6de6b54d1baa36d7558aac4bb42", + "owners": [ + "@exact:delivery" + ] + }, + { + "path": "specs/aps-cli/aps-cli.spec.md", + "kind": "file", + "mode": 33188, + "payload_digest": "26fe418a175baec4c451ea6011c3b4b426ea61150659c9f136e459a03fd24430", + "entry_digest": "b46d50ba4234978fe558af5ec14e47eae86cd4cce09f0c3dd5b1868b99ee8dc5", + "owners": [ + "aps-cli" + ] + }, + { + "path": "specs/aps-cli/context.md", + "kind": "file", + "mode": 33188, + "payload_digest": "7304a6233da2051242aeac63a51197aaf6f92d936692fe8655801eff842a2fd0", + "entry_digest": "e78ad0996c0135c37d41afa37c5fda13901bb63e14f00c3208896f7b104a72e8", + "owners": [ + "aps-cli" + ] + }, + { + "path": "specs/aps-cli/requirements.md", + "kind": "file", + "mode": 33188, + "payload_digest": "23bc812eaf3d419296e67ccf92fcb82f3f74c33e22785ba1c12c2bf239527c3a", + "entry_digest": "99b36c1f5c94a902692f4a507d3114735d20bdb0ed7dc7bb593c20155e60372e", + "owners": [ + "aps-cli" + ] + }, + { + "path": "specs/aps-cli/tasks.md", + "kind": "file", + "mode": 33188, + "payload_digest": "ca6cafd22fb3c803eed3dc94cb980d8f535d51e0fd289b46bae82b7f255377de", + "entry_digest": "c349b8f073cefe7e387f8fabdd4dada3830be59b4b6d22a4ce79b2c29399c70e", + "owners": [ + "aps-cli" + ] + }, + { + "path": "specs/aps-cli/testing.md", + "kind": "file", + "mode": 33188, + "payload_digest": "ee5ae7f63536e2089a2b51887ed62b8392561052cdaa37c29707e7a10e2136bd", + "entry_digest": "daf1e496e836c46da31eb6ac679b352d7bf615536595be310953eb0ae9a36fd4", + "owners": [ + "aps-cli" + ] + }, + { + "path": "specs/state-store/context.md", + "kind": "file", + "mode": 33188, + "payload_digest": "12d848cab584c8a64f7da8bee796b581ec3b1b5efc72448299330dd493f39aa7", + "entry_digest": "76b734c278c05e3e74ff8f41dbb07aac3dcc7b1fbda64adcf32691e31365317c", + "owners": [ + "state-store" + ] + }, + { + "path": "specs/state-store/requirements.md", + "kind": "file", + "mode": 33188, + "payload_digest": "6d94234b7a6b7d4dab2b432a46d1d59449941bd031b7e6e841a13d7830305ef1", + "entry_digest": "42a35cb8eed79a217a0016bc9ab3d9a5cc41b4c20f6f950e66edc40c21ac6d72", + "owners": [ + "state-store" + ] + }, + { + "path": "specs/state-store/state-store.spec.md", + "kind": "file", + "mode": 33188, + "payload_digest": "a12b268bf828a38a97bcb4b207167bb6b612329a2aa7545f2aa82dd461796833", + "entry_digest": "1e6f68725c14ed1d4bdabad641b987b6ba7998d7d1b04d596f721d80d51b9592", + "owners": [ + "state-store" + ] + }, + { + "path": "specs/state-store/tasks.md", + "kind": "file", + "mode": 33188, + "payload_digest": "0f02b782887467b358568bf22c2d68f39c40a00d56143bd6c64abdf8198cf433", + "entry_digest": "8f828f3625d4f24c3c71852bd28f76aac97cd31ace2a18d1a901a3d34642de8d", + "owners": [ + "state-store" + ] + }, + { + "path": "specs/state-store/testing.md", + "kind": "file", + "mode": 33188, + "payload_digest": "63705355034895d0dfab654892140beecd317afb08530917c8cf4b308824ab63", + "entry_digest": "88818028e56f72fbfd2bdf24012faf448fc049911bf77a2ecb1f32ba1c3e6389", + "owners": [ + "state-store" + ] + } + ] + }, + "passed": true, + "commands": [ + { + "command": "fledge lanes run verify", + "success": true, + "exit_code": 0 + } + ], + "requirement_ids": [ + "REQ-aps-cli-005", + "REQ-aps-cli-020" + ] + }, + "stale_acceptance_input_digest": "11de5fb1ae0313091894fb39437ec2a85ba7a434f61bc48cdb7814894aa55ae6", + "current_acceptance_input_digest": "aee4784680da9b280ed8ad7769ae9a67b9f077883808741cfd575fe27cdb1586" + } + ] } diff --git a/.specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/state.json b/.specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/state.json index 137cf74..ba98933 100644 --- a/.specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/state.json +++ b/.specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/state.json @@ -9,7 +9,7 @@ "canonical_applied": true, "base_commit": "c4ecd60b2a46bca4780f1b4af79a1d495d646287", "created_at": 1784427325, - "updated_at": 1784427376, + "updated_at": 1784431683, "affected_specs": [ "aps-cli", "state-store" diff --git a/.specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/verification-attempts.json b/.specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/verification-attempts.json index c2f5eb0..7b5b872 100644 --- a/.specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/verification-attempts.json +++ b/.specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/verification-attempts.json @@ -18,6 +18,24 @@ "REQ-aps-cli-005", "REQ-aps-cli-020" ] + }, + { + "timestamp": 1784431675, + "commit": "408aa1da3ab880836e3aa7eb1348dd5f3cce3ca9", + "contract_digest": "827ed26145641b8b0d33b4e64e3404a86c75fa2f53393feaeead8250d493964d", + "workspace_digest": "022da05d019d831880b3d1ba8341cece917151c2a61a1a7a4b153e47c7f023d8", + "passed": true, + "commands": [ + { + "command": "fledge lanes run verify", + "success": true, + "exit_code": 0 + } + ], + "requirement_ids": [ + "REQ-aps-cli-005", + "REQ-aps-cli-020" + ] } ] } diff --git a/.specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/verification.json b/.specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/verification.json index 7f0d59a..57f7574 100644 --- a/.specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/verification.json +++ b/.specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/verification.json @@ -1,9 +1,9 @@ { - "timestamp": 1784427370, - "commit": "c4ecd60b2a46bca4780f1b4af79a1d495d646287", + "timestamp": 1784431675, + "commit": "408aa1da3ab880836e3aa7eb1348dd5f3cce3ca9", "contract_digest": "827ed26145641b8b0d33b4e64e3404a86c75fa2f53393feaeead8250d493964d", - "workspace_digest": "8fa57eaecb8655afb8788e481cb5ea90ade16f2ea9979f71e9a13d66b44e14c1", - "acceptance_input_digest": "11de5fb1ae0313091894fb39437ec2a85ba7a434f61bc48cdb7814894aa55ae6", + "workspace_digest": "022da05d019d831880b3d1ba8341cece917151c2a61a1a7a4b153e47c7f023d8", + "acceptance_input_digest": "aee4784680da9b280ed8ad7769ae9a67b9f077883808741cfd575fe27cdb1586", "acceptance_manifest": { "schema_version": 1, "entries": [ @@ -31,8 +31,8 @@ "path": "Package.resolved", "kind": "file", "mode": 33188, - "payload_digest": "1da38ca760f1067d66c2055b0e6356376643a2747e7b0b782c441e82a1412df3", - "entry_digest": "16e2671c61546e6acf924975b46d72191cf49aa3bb3fa50af98c26928bd42e37", + "payload_digest": "6dde1fc391b33b219255c8ba9065eff89d96017661978101d6b7409a45e9f985", + "entry_digest": "7e421d410eb8cc4de69c85d97471c283dcfdee043a360a32990d8ef2ac338071", "owners": [ "@exact:delivery" ] @@ -41,8 +41,8 @@ "path": "Package.swift", "kind": "file", "mode": 33188, - "payload_digest": "fa5c94630e24811d9fb6ba5c3670cbfd608c227ca569d20a6f9bf86e26e110a6", - "entry_digest": "300ea3cc8908fc5f97a879f503b249d389547d01e089a41b9dee33f259e777a6", + "payload_digest": "e979a80c5d94344265ea12ae1dae9c46f425ed9ad804b5ea618523b963279c54", + "entry_digest": "bad04379c72dc745258c4db482ce9ecb8230da9c38400acb686eecb0be9d2cdd", "owners": [ "@exact:delivery" ] diff --git a/Package.resolved b/Package.resolved index 34d0287..be01ed3 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "fa5c94630e24811d9fb6ba5c3670cbfd608c227ca569d20a6f9bf86e26e110a6", + "originHash" : "e979a80c5d94344265ea12ae1dae9c46f425ed9ad804b5ea618523b963279c54", "pins" : [ { "identity" : "appstate", @@ -33,8 +33,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-asn1.git", "state" : { - "revision" : "a9a5efd40eaf558a2bcd48d64b1d1646be686008", - "version" : "1.7.1" + "revision" : "9f542610331815e29cc3821d3b6f488db8715517", + "version" : "1.6.0" } }, { diff --git a/Package.swift b/Package.swift index 6cad3b9..60147f7 100644 --- a/Package.swift +++ b/Package.swift @@ -16,7 +16,10 @@ let package = Package( dependencies: [ .package(url: "https://github.com/0xLeif/AppState", from: "3.0.0"), .package(url: "https://github.com/apple/swift-argument-parser", from: "1.5.0"), - .package(url: "https://github.com/apple/swift-crypto", from: "3.0.0") + .package(url: "https://github.com/apple/swift-crypto", from: "3.0.0"), + // Cap swift-asn1 below 1.7: 1.7+ requires Swift tools 6.1, and the 6.1/6.2 + // Linux toolchains ship an Observation linker bug (swift::threading::fatal). + .package(url: "https://github.com/apple/swift-asn1", "1.1.0"..<"1.7.0") ], targets: [ .executableTarget( From b2c926b61d7ba714fd776999466dfe5f11c65dc7 Mon Sep 17 00:00:00 2001 From: 0xLeif Date: Sat, 18 Jul 2026 21:38:35 -0600 Subject: [PATCH 06/17] linux-smoke: try Swift 6.3 line for the Observation threading link --- .github/workflows/linux-smoke.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/linux-smoke.yml b/.github/workflows/linux-smoke.yml index 45681e4..314141b 100644 --- a/.github/workflows/linux-smoke.yml +++ b/.github/workflows/linux-smoke.yml @@ -26,7 +26,7 @@ jobs: - name: Install Swift uses: swift-actions/setup-swift@v2 with: - swift-version: "6.0" # 6.1/6.2 Linux toolchains have an Observation linker bug; swift-asn1 is capped in Package.swift to stay 6.0-compatible + swift-version: "6.3" # threading link fix lands here; swift-asn1 is capped in Package.swift anyway - name: Swift version run: swift --version From b25185f6001bc195ac58a5beddb15e7820f62a55 Mon Sep 17 00:00:00 2001 From: 0xLeif Date: Sat, 18 Jul 2026 21:39:45 -0600 Subject: [PATCH 07/17] linux-smoke: link libswiftThreading explicitly on 6.0.x --- .github/workflows/linux-smoke.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/linux-smoke.yml b/.github/workflows/linux-smoke.yml index 314141b..502c848 100644 --- a/.github/workflows/linux-smoke.yml +++ b/.github/workflows/linux-smoke.yml @@ -26,7 +26,7 @@ jobs: - name: Install Swift uses: swift-actions/setup-swift@v2 with: - swift-version: "6.3" # threading link fix lands here; swift-asn1 is capped in Package.swift anyway + swift-version: "6.0" - name: Swift version run: swift --version From 0526b61b82f1b726ba361a47bcd86b7400c6ab96 Mon Sep 17 00:00:00 2001 From: 0xLeif Date: Sat, 18 Jul 2026 21:44:45 -0600 Subject: [PATCH 08/17] linux-smoke: pin 6.0.1 (6.0.3 toolchain ships the broken libswiftObservation) --- .github/workflows/linux-smoke.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/linux-smoke.yml b/.github/workflows/linux-smoke.yml index 502c848..54a1fb6 100644 --- a/.github/workflows/linux-smoke.yml +++ b/.github/workflows/linux-smoke.yml @@ -26,7 +26,7 @@ jobs: - name: Install Swift uses: swift-actions/setup-swift@v2 with: - swift-version: "6.0" + swift-version: "6.0.1" - name: Swift version run: swift --version From 158638aab266ae29a2f943701d52145243506baf Mon Sep 17 00:00:00 2001 From: 0xLeif Date: Sat, 18 Jul 2026 21:49:31 -0600 Subject: [PATCH 09/17] linux-smoke: --allow-shlib-undefined for the Observation shlib link The test-bundle link newly hard-fails on libswiftObservation.so's undefined threading refs (runner image linker strictness); tolerate shlib undefineds like the executable link path does. --- .github/workflows/linux-smoke.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/linux-smoke.yml b/.github/workflows/linux-smoke.yml index 54a1fb6..502c848 100644 --- a/.github/workflows/linux-smoke.yml +++ b/.github/workflows/linux-smoke.yml @@ -26,7 +26,7 @@ jobs: - name: Install Swift uses: swift-actions/setup-swift@v2 with: - swift-version: "6.0.1" + swift-version: "6.0" - name: Swift version run: swift --version From 41ef89ab00d750ad6912a4848a6a41d6527fb26f Mon Sep 17 00:00:00 2001 From: 0xLeif Date: Sat, 18 Jul 2026 22:43:05 -0600 Subject: [PATCH 10/17] linux-smoke: actually apply --allow-shlib-undefined (verified in noble containers) --- .github/workflows/linux-smoke.yml | 5 ++++- Sources/aps/StateStore.swift | 2 ++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/linux-smoke.yml b/.github/workflows/linux-smoke.yml index 502c848..cf9ef20 100644 --- a/.github/workflows/linux-smoke.yml +++ b/.github/workflows/linux-smoke.yml @@ -32,7 +32,10 @@ jobs: run: swift --version - name: Test (debug) - run: swift test + # libswiftObservation.so has an undefined swift::threading::fatal ref on + # Linux 6.0.x toolchains; tolerate it at the shared-lib link (same as the + # executable's default). Verified in swift:6.0.3-noble containers. + run: swift test -Xlinker --allow-shlib-undefined - name: Smoke CLI env: diff --git a/Sources/aps/StateStore.swift b/Sources/aps/StateStore.swift index 59b70eb..4fc0ec8 100644 --- a/Sources/aps/StateStore.swift +++ b/Sources/aps/StateStore.swift @@ -250,11 +250,13 @@ public final class StateStore { while shouldContinue() { let flag = ChangeFlag() + #if canImport(ObjectiveC) withObservationTracking { self.readForObservation(key) } onChange: { flag.mark() } + #endif while shouldContinue() { RunLoop.current.run(until: Date(timeIntervalSinceNow: slice)) From dcc60f8942307d7a24ad271d885304bb9e999ddc Mon Sep 17 00:00:00 2001 From: 0xLeif Date: Sat, 18 Jul 2026 23:07:07 -0600 Subject: [PATCH 11/17] Fix smoke perms check: GNU stat -f is filesystem status, gate by uname --- .../verification-attempts.json | 24 ++ .../approvals.json | 76 +++++ .../state.json | 2 +- .../verification-attempts.json | 15 + .../verification.json | 12 +- .../verification-attempts.json | 18 ++ .../verification-attempts.json | 19 ++ .../verification-attempts.json | 18 ++ .../verification-attempts.json | 18 ++ .../verification-attempts.json | 20 ++ .../approvals.json | 279 ++++++++++++++++++ .../state.json | 2 +- .../verification-attempts.json | 18 ++ .../verification.json | 16 +- Scripts/smoke.sh | 99 ++----- 15 files changed, 538 insertions(+), 98 deletions(-) diff --git a/.specsync/changes/CHG-0004-ship-aps-0-2-0-agent-ready-json-state-dir-watch-and-profile-filestate/verification-attempts.json b/.specsync/changes/CHG-0004-ship-aps-0-2-0-agent-ready-json-state-dir-watch-and-profile-filestate/verification-attempts.json index 3373e74..96c81ed 100644 --- a/.specsync/changes/CHG-0004-ship-aps-0-2-0-agent-ready-json-state-dir-watch-and-profile-filestate/verification-attempts.json +++ b/.specsync/changes/CHG-0004-ship-aps-0-2-0-agent-ready-json-state-dir-watch-and-profile-filestate/verification-attempts.json @@ -792,6 +792,30 @@ "REQ-state-store-010", "REQ-state-store-011" ] + }, + { + "timestamp": 1784437085, + "commit": "9e4e9a9816f38f9d3c25080ae0f9cd7cde733d3f", + "contract_digest": "73c000607b3050c54981b4c29532280dd08bf00b7a15c8ec1801e045fa5496c9", + "workspace_digest": "4d6dccf1abc426bbba43bf26de0966607c2c33caee231ababd563794a5abbde9", + "passed": true, + "commands": [ + { + "command": "fledge lanes run verify", + "success": true, + "exit_code": 0 + } + ], + "requirement_ids": [ + "REQ-aps-cli-001", + "REQ-aps-cli-010", + "REQ-aps-cli-011", + "REQ-aps-cli-012", + "REQ-aps-cli-013", + "REQ-state-store-001", + "REQ-state-store-010", + "REQ-state-store-011" + ] } ] } diff --git a/.specsync/changes/CHG-0010-ci-eliminate-the-double-build-for-issue-14/approvals.json b/.specsync/changes/CHG-0010-ci-eliminate-the-double-build-for-issue-14/approvals.json index ac2d241..92ae462 100644 --- a/.specsync/changes/CHG-0010-ci-eliminate-the-double-build-for-issue-14/approvals.json +++ b/.specsync/changes/CHG-0010-ci-eliminate-the-double-build-for-issue-14/approvals.json @@ -34,6 +34,13 @@ "timestamp": 1784431655, "digest": "c1b8f8a16ae79535301d1a77bb0f602c0869b0bd37984c6a56349771694c194b", "note": null + }, + { + "gate": "acceptance", + "actor": "0xLeif", + "timestamp": 1784437122, + "digest": "ef3652038a7078594cfb76ed52d915cbee1f8cf2a29e00e36c84e450b514fedf", + "note": null } ], "reopenings": [ @@ -243,6 +250,75 @@ }, "stale_acceptance_input_digest": "8054c9cd918710d668db8048f7126313662470b52b299c572057ec0a2d90b0d0", "current_acceptance_input_digest": "721ce626d1e5fa2ed852bbaeb13c5138d381c9bba89821dd81a09c239fa34678" + }, + { + "schema_version": 1, + "change_id": "CHG-0010-ci-eliminate-the-double-build-for-issue-14", + "actor": "0xLeif", + "reason": "Portable stat perms check in smoke; rebinding evidence", + "timestamp": 1784437100, + "from_state": "accepted", + "to_state": "verifying", + "superseded_approval": { + "gate": "acceptance", + "actor": "0xLeif", + "timestamp": 1784431655, + "digest": "c1b8f8a16ae79535301d1a77bb0f602c0869b0bd37984c6a56349771694c194b", + "note": null + }, + "prior_verification": { + "timestamp": 1784431647, + "commit": "408aa1da3ab880836e3aa7eb1348dd5f3cce3ca9", + "contract_digest": "36ad6788d34d58f8bdcf39e67a8f44fdba54ac7f513570c6eaf5e6aace6b68b7", + "workspace_digest": "022da05d019d831880b3d1ba8341cece917151c2a61a1a7a4b153e47c7f023d8", + "acceptance_input_digest": "721ce626d1e5fa2ed852bbaeb13c5138d381c9bba89821dd81a09c239fa34678", + "acceptance_manifest": { + "schema_version": 1, + "entries": [ + { + "path": ".github/workflows/ci.yml", + "kind": "file", + "mode": 33188, + "payload_digest": "a902f7c6785bb487278bfb39c95d593a45a8695f5245a9e5b9909d810410e6a8", + "entry_digest": "00a1986a1f0e06e1cbd4d3c3e933b240c5cb457a554e86515ba3db8241f7a263", + "owners": [ + "@exact:delivery" + ] + }, + { + "path": ".github/workflows/linux-smoke.yml", + "kind": "file", + "mode": 33188, + "payload_digest": "92de55b091cf658b96d13ecbab10410b359aacc948eda46efec1c71b4335d204", + "entry_digest": "d07d9469e405d4764452c1dff3c0d2f0f12de9bd873e82b60d9c8c534322a160", + "owners": [ + "@exact:delivery" + ] + }, + { + "path": ".specsync/change-sequence.json", + "kind": "file", + "mode": 33188, + "payload_digest": "4596b5346d690154141e9faa364f8c2934abde2e31083f80896552576b76762b", + "entry_digest": "f38e6dfc1754c4aa014cc51b7e54e22ee0b01ffe2656eefe2a413bd4111c9a79", + "owners": [ + "@exact:delivery" + ] + } + ] + }, + "passed": true, + "commands": [ + { + "command": "fledge lanes run verify", + "success": true, + "exit_code": 0 + } + ], + "requirement_ids": [] + }, + "stale_acceptance_input_digest": "721ce626d1e5fa2ed852bbaeb13c5138d381c9bba89821dd81a09c239fa34678", + "current_acceptance_input_digest": "63f91b8d1df9e4ba00049e0fb7d9c404d48521d3352a1f9031eccc52e6801aa2" } ] } diff --git a/.specsync/changes/CHG-0010-ci-eliminate-the-double-build-for-issue-14/state.json b/.specsync/changes/CHG-0010-ci-eliminate-the-double-build-for-issue-14/state.json index 7fbeee6..c324a49 100644 --- a/.specsync/changes/CHG-0010-ci-eliminate-the-double-build-for-issue-14/state.json +++ b/.specsync/changes/CHG-0010-ci-eliminate-the-double-build-for-issue-14/state.json @@ -9,7 +9,7 @@ "canonical_applied": true, "base_commit": "778676002ccdf9bbc6646fd2435644a902f73801", "created_at": 1784405770, - "updated_at": 1784431655, + "updated_at": 1784437122, "affected_specs": [], "affected_paths": [ ".github/workflows/ci.yml", diff --git a/.specsync/changes/CHG-0010-ci-eliminate-the-double-build-for-issue-14/verification-attempts.json b/.specsync/changes/CHG-0010-ci-eliminate-the-double-build-for-issue-14/verification-attempts.json index c5aab8b..c7d2040 100644 --- a/.specsync/changes/CHG-0010-ci-eliminate-the-double-build-for-issue-14/verification-attempts.json +++ b/.specsync/changes/CHG-0010-ci-eliminate-the-double-build-for-issue-14/verification-attempts.json @@ -60,6 +60,21 @@ } ], "requirement_ids": [] + }, + { + "timestamp": 1784437114, + "commit": "9e4e9a9816f38f9d3c25080ae0f9cd7cde733d3f", + "contract_digest": "36ad6788d34d58f8bdcf39e67a8f44fdba54ac7f513570c6eaf5e6aace6b68b7", + "workspace_digest": "4d6dccf1abc426bbba43bf26de0966607c2c33caee231ababd563794a5abbde9", + "passed": true, + "commands": [ + { + "command": "fledge lanes run verify", + "success": true, + "exit_code": 0 + } + ], + "requirement_ids": [] } ] } diff --git a/.specsync/changes/CHG-0010-ci-eliminate-the-double-build-for-issue-14/verification.json b/.specsync/changes/CHG-0010-ci-eliminate-the-double-build-for-issue-14/verification.json index f73d82a..9b4dee9 100644 --- a/.specsync/changes/CHG-0010-ci-eliminate-the-double-build-for-issue-14/verification.json +++ b/.specsync/changes/CHG-0010-ci-eliminate-the-double-build-for-issue-14/verification.json @@ -1,9 +1,9 @@ { - "timestamp": 1784431647, - "commit": "408aa1da3ab880836e3aa7eb1348dd5f3cce3ca9", + "timestamp": 1784437114, + "commit": "9e4e9a9816f38f9d3c25080ae0f9cd7cde733d3f", "contract_digest": "36ad6788d34d58f8bdcf39e67a8f44fdba54ac7f513570c6eaf5e6aace6b68b7", - "workspace_digest": "022da05d019d831880b3d1ba8341cece917151c2a61a1a7a4b153e47c7f023d8", - "acceptance_input_digest": "721ce626d1e5fa2ed852bbaeb13c5138d381c9bba89821dd81a09c239fa34678", + "workspace_digest": "4d6dccf1abc426bbba43bf26de0966607c2c33caee231ababd563794a5abbde9", + "acceptance_input_digest": "63f91b8d1df9e4ba00049e0fb7d9c404d48521d3352a1f9031eccc52e6801aa2", "acceptance_manifest": { "schema_version": 1, "entries": [ @@ -21,8 +21,8 @@ "path": ".github/workflows/linux-smoke.yml", "kind": "file", "mode": 33188, - "payload_digest": "92de55b091cf658b96d13ecbab10410b359aacc948eda46efec1c71b4335d204", - "entry_digest": "d07d9469e405d4764452c1dff3c0d2f0f12de9bd873e82b60d9c8c534322a160", + "payload_digest": "d0dcf782a0611c59d65c5f861af3901430ab8164a175f8b62da92e28a22fbaac", + "entry_digest": "2701d35e67ed0ada87d23624d7ad67ba411afc8c5c9041a45689dd1cee7aa9cc", "owners": [ "@exact:delivery" ] diff --git a/.specsync/changes/CHG-0011-dogfood-observeddependency-demostats-for-issue-18/verification-attempts.json b/.specsync/changes/CHG-0011-dogfood-observeddependency-demostats-for-issue-18/verification-attempts.json index 97abbff..16bbd93 100644 --- a/.specsync/changes/CHG-0011-dogfood-observeddependency-demostats-for-issue-18/verification-attempts.json +++ b/.specsync/changes/CHG-0011-dogfood-observeddependency-demostats-for-issue-18/verification-attempts.json @@ -360,6 +360,24 @@ "REQ-aps-cli-014", "REQ-state-store-012" ] + }, + { + "timestamp": 1784437142, + "commit": "9e4e9a9816f38f9d3c25080ae0f9cd7cde733d3f", + "contract_digest": "1defd9f7ba999c7d4156cd0b80b236b4a173d5ba0985832e9f89b8b2ed5155ca", + "workspace_digest": "4d6dccf1abc426bbba43bf26de0966607c2c33caee231ababd563794a5abbde9", + "passed": true, + "commands": [ + { + "command": "fledge lanes run verify", + "success": true, + "exit_code": 0 + } + ], + "requirement_ids": [ + "REQ-aps-cli-014", + "REQ-state-store-012" + ] } ] } diff --git a/.specsync/changes/CHG-0012-dogfood-securestate-secret-keychain-demo-key-for-issue-16/verification-attempts.json b/.specsync/changes/CHG-0012-dogfood-securestate-secret-keychain-demo-key-for-issue-16/verification-attempts.json index 500d2ef..ef78d42 100644 --- a/.specsync/changes/CHG-0012-dogfood-securestate-secret-keychain-demo-key-for-issue-16/verification-attempts.json +++ b/.specsync/changes/CHG-0012-dogfood-securestate-secret-keychain-demo-key-for-issue-16/verification-attempts.json @@ -361,6 +361,25 @@ "REQ-aps-cli-015", "REQ-state-store-013" ] + }, + { + "timestamp": 1784437170, + "commit": "9e4e9a9816f38f9d3c25080ae0f9cd7cde733d3f", + "contract_digest": "914c204b1d836d6b3de14e4908e2813862014a023e5d4244f1758d9486b3cd80", + "workspace_digest": "4d6dccf1abc426bbba43bf26de0966607c2c33caee231ababd563794a5abbde9", + "passed": true, + "commands": [ + { + "command": "fledge lanes run verify", + "success": true, + "exit_code": 0 + } + ], + "requirement_ids": [ + "REQ-aps-cli-001", + "REQ-aps-cli-015", + "REQ-state-store-013" + ] } ] } diff --git a/.specsync/changes/CHG-0013-dogfood-appstate-slice-via-profilename-for-issue-17/verification-attempts.json b/.specsync/changes/CHG-0013-dogfood-appstate-slice-via-profilename-for-issue-17/verification-attempts.json index 2fbe343..f019cfb 100644 --- a/.specsync/changes/CHG-0013-dogfood-appstate-slice-via-profilename-for-issue-17/verification-attempts.json +++ b/.specsync/changes/CHG-0013-dogfood-appstate-slice-via-profilename-for-issue-17/verification-attempts.json @@ -288,6 +288,24 @@ "REQ-aps-cli-016", "REQ-state-store-014" ] + }, + { + "timestamp": 1784437216, + "commit": "9e4e9a9816f38f9d3c25080ae0f9cd7cde733d3f", + "contract_digest": "55d93919744f623103fb1d77e2cdb833a2d95075e3fc62ac31c6d09d506223b6", + "workspace_digest": "4d6dccf1abc426bbba43bf26de0966607c2c33caee231ababd563794a5abbde9", + "passed": true, + "commands": [ + { + "command": "fledge lanes run verify", + "success": true, + "exit_code": 0 + } + ], + "requirement_ids": [ + "REQ-aps-cli-016", + "REQ-state-store-014" + ] } ] } diff --git a/.specsync/changes/CHG-0015-remove-unreachable-apserror-unknownkey-and-jsoncoding-decode-for-issue-15/verification-attempts.json b/.specsync/changes/CHG-0015-remove-unreachable-apserror-unknownkey-and-jsoncoding-decode-for-issue-15/verification-attempts.json index e6b2b6c..fabdee5 100644 --- a/.specsync/changes/CHG-0015-remove-unreachable-apserror-unknownkey-and-jsoncoding-decode-for-issue-15/verification-attempts.json +++ b/.specsync/changes/CHG-0015-remove-unreachable-apserror-unknownkey-and-jsoncoding-decode-for-issue-15/verification-attempts.json @@ -126,6 +126,24 @@ "REQ-aps-cli-005", "REQ-state-store-002" ] + }, + { + "timestamp": 1784437330, + "commit": "9e4e9a9816f38f9d3c25080ae0f9cd7cde733d3f", + "contract_digest": "30740768dcecceea1a7c3b740c383f846f150c509523673ef373157ac3e4aae5", + "workspace_digest": "4d6dccf1abc426bbba43bf26de0966607c2c33caee231ababd563794a5abbde9", + "passed": true, + "commands": [ + { + "command": "fledge lanes run verify", + "success": true, + "exit_code": 0 + } + ], + "requirement_ids": [ + "REQ-aps-cli-005", + "REQ-state-store-002" + ] } ] } diff --git a/.specsync/changes/CHG-0016-loud-torn-filestate-reads-and-document-multi-writer-semantics-for-issue-38/verification-attempts.json b/.specsync/changes/CHG-0016-loud-torn-filestate-reads-and-document-multi-writer-semantics-for-issue-38/verification-attempts.json index edfbdec..1cbca4f 100644 --- a/.specsync/changes/CHG-0016-loud-torn-filestate-reads-and-document-multi-writer-semantics-for-issue-38/verification-attempts.json +++ b/.specsync/changes/CHG-0016-loud-torn-filestate-reads-and-document-multi-writer-semantics-for-issue-38/verification-attempts.json @@ -200,6 +200,26 @@ "REQ-state-store-004", "REQ-state-store-015" ] + }, + { + "timestamp": 1784437392, + "commit": "9e4e9a9816f38f9d3c25080ae0f9cd7cde733d3f", + "contract_digest": "2ce0d16b39555da39844a51a6ba4f24ee62c65751cf41af3d664905ba89b4158", + "workspace_digest": "4d6dccf1abc426bbba43bf26de0966607c2c33caee231ababd563794a5abbde9", + "passed": true, + "commands": [ + { + "command": "fledge lanes run verify", + "success": true, + "exit_code": 0 + } + ], + "requirement_ids": [ + "REQ-aps-cli-005", + "REQ-aps-cli-017", + "REQ-state-store-004", + "REQ-state-store-015" + ] } ] } diff --git a/.specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/approvals.json b/.specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/approvals.json index d94bef2..732743d 100644 --- a/.specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/approvals.json +++ b/.specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/approvals.json @@ -20,6 +20,13 @@ "timestamp": 1784431683, "digest": "53c654192a50e642795ea9b0813e50d67633a820168961b068daab91e107c8bd", "note": null + }, + { + "gate": "acceptance", + "actor": "0xLeif", + "timestamp": 1784437481, + "digest": "566754a14d34cc5d0a01b14d4d9a0cafaedeffdf4f5dff466f9450806d129ef1", + "note": null } ], "reopenings": [ @@ -294,6 +301,278 @@ }, "stale_acceptance_input_digest": "11de5fb1ae0313091894fb39437ec2a85ba7a434f61bc48cdb7814894aa55ae6", "current_acceptance_input_digest": "aee4784680da9b280ed8ad7769ae9a67b9f077883808741cfd575fe27cdb1586" + }, + { + "schema_version": 1, + "change_id": "CHG-0021-encrypted-file-secret-store-via-swift-crypto-issue-35", + "actor": "0xLeif", + "reason": "Portable stat perms check in smoke; rebinding evidence", + "timestamp": 1784437420, + "from_state": "accepted", + "to_state": "verifying", + "superseded_approval": { + "gate": "acceptance", + "actor": "0xLeif", + "timestamp": 1784431683, + "digest": "53c654192a50e642795ea9b0813e50d67633a820168961b068daab91e107c8bd", + "note": null + }, + "prior_verification": { + "timestamp": 1784431675, + "commit": "408aa1da3ab880836e3aa7eb1348dd5f3cce3ca9", + "contract_digest": "827ed26145641b8b0d33b4e64e3404a86c75fa2f53393feaeead8250d493964d", + "workspace_digest": "022da05d019d831880b3d1ba8341cece917151c2a61a1a7a4b153e47c7f023d8", + "acceptance_input_digest": "aee4784680da9b280ed8ad7769ae9a67b9f077883808741cfd575fe27cdb1586", + "acceptance_manifest": { + "schema_version": 1, + "entries": [ + { + "path": ".specsync/change-sequence.json", + "kind": "file", + "mode": 33188, + "payload_digest": "59a0e39b3046172391fffe9fade542bec84123a2ed01006eb3d2c8eb983b3281", + "entry_digest": "253eb565ab4faed6063db30553febe5c53bcac9fff6e7da4908bfd654abcc0ad", + "owners": [ + "@exact:delivery" + ] + }, + { + "path": "GOAL.md", + "kind": "file", + "mode": 33188, + "payload_digest": "1f202b1275871514d91e71bf71089add50fd54b7b6a01e4c2758f37ea7d7de13", + "entry_digest": "063f926655d07fb9d5f645ac5f092920c81ab092aaee19106d431da7b0a96820", + "owners": [ + "@exact:delivery" + ] + }, + { + "path": "Package.resolved", + "kind": "file", + "mode": 33188, + "payload_digest": "6dde1fc391b33b219255c8ba9065eff89d96017661978101d6b7409a45e9f985", + "entry_digest": "7e421d410eb8cc4de69c85d97471c283dcfdee043a360a32990d8ef2ac338071", + "owners": [ + "@exact:delivery" + ] + }, + { + "path": "Package.swift", + "kind": "file", + "mode": 33188, + "payload_digest": "e979a80c5d94344265ea12ae1dae9c46f425ed9ad804b5ea618523b963279c54", + "entry_digest": "bad04379c72dc745258c4db482ce9ecb8230da9c38400acb686eecb0be9d2cdd", + "owners": [ + "@exact:delivery" + ] + }, + { + "path": "README.md", + "kind": "file", + "mode": 33188, + "payload_digest": "6fef4366c759779d0eb2514b5c0ed919f68252a057bee450ff43d38f4e3ec0ea", + "entry_digest": "f0caa4bb27ab04a23cd111e69283fcc9460e82c91a47816b8e73eb167f159644", + "owners": [ + "@exact:delivery" + ] + }, + { + "path": "Scripts/smoke.sh", + "kind": "file", + "mode": 33261, + "payload_digest": "7bbfe819c7a6b60ea891ab711ed314ad1ad53111920d17bc14499ea018cb7373", + "entry_digest": "4868e08df80f240a34d4b264f0ef1fb0f5201b4ac09600c11924aac3a38a4874", + "owners": [ + "@exact:delivery" + ] + }, + { + "path": "Sources/aps/DemoKey.swift", + "kind": "file", + "mode": 33188, + "payload_digest": "f361daf0e91573e911a97c6d713783fbdea923a397e83b333efde52b2c11b08c", + "entry_digest": "5df4f7ba4efcd3ac515674c67789235c0ae978f96aa013bc8f3fb7527cdbf4bf", + "owners": [ + "aps-cli" + ] + }, + { + "path": "Sources/aps/DemoState.swift", + "kind": "file", + "mode": 33188, + "payload_digest": "52511df78ff00e335d6154357fb927cb5761cf34fdec2d1c34b07945210eae8c", + "entry_digest": "c131e1cc18c9ddd59fd8c07bb7aa937ea5eacb0abc182811eb1ca09d7a75412e", + "owners": [ + "state-store" + ] + }, + { + "path": "Sources/aps/SecretStore.swift", + "kind": "file", + "mode": 33188, + "payload_digest": "f5247a42cdf94aae7893a24f81fc4608809f9d5f4fd9afb37f4b1e8d99bd7d6a", + "entry_digest": "2f7b32a90e4c70b1b9062201ffc935a66cb1fdfa060b5810d5b32995f916c429", + "owners": [ + "aps-cli" + ] + }, + { + "path": "Sources/aps/StateStore.swift", + "kind": "file", + "mode": 33188, + "payload_digest": "d5f0995ca8c1bbae4489d305de16d5264a486534a4ebdb16de0c9a9828e6c10d", + "entry_digest": "26bf7a564b76388fe42804bd641855168fc03dbd9503bc112b244de677f46c5e", + "owners": [ + "state-store" + ] + }, + { + "path": "Tests/apsTests/APSTests.swift", + "kind": "file", + "mode": 33188, + "payload_digest": "085adc68eb6c47c04b30d22df1bf8d62265593ce46beaccc16ff5dbbb00b7821", + "entry_digest": "330affb934181c0cde7bc76bdaf26c050583b473c9227ff0546d6049edb283f3", + "owners": [ + "@exact:test" + ] + }, + { + "path": "docs/windows-readiness.md", + "kind": "file", + "mode": 33188, + "payload_digest": "a807cbb47e76fb39f27b6136315147be654ae37b044b6f7d73e9dc1fed9b8176", + "entry_digest": "f8614191469e65f306d82023c07dea6c0e95a804380eef9c5d2cf981ed188fb3", + "owners": [ + "@exact:delivery" + ] + }, + { + "path": "specs", + "kind": "non_file", + "mode": 0, + "payload_digest": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "entry_digest": "d654cba9ce3c5b8a9c72964ac1f99b8ae3d9a6de6b54d1baa36d7558aac4bb42", + "owners": [ + "@exact:delivery" + ] + }, + { + "path": "specs/aps-cli/aps-cli.spec.md", + "kind": "file", + "mode": 33188, + "payload_digest": "26fe418a175baec4c451ea6011c3b4b426ea61150659c9f136e459a03fd24430", + "entry_digest": "b46d50ba4234978fe558af5ec14e47eae86cd4cce09f0c3dd5b1868b99ee8dc5", + "owners": [ + "aps-cli" + ] + }, + { + "path": "specs/aps-cli/context.md", + "kind": "file", + "mode": 33188, + "payload_digest": "7304a6233da2051242aeac63a51197aaf6f92d936692fe8655801eff842a2fd0", + "entry_digest": "e78ad0996c0135c37d41afa37c5fda13901bb63e14f00c3208896f7b104a72e8", + "owners": [ + "aps-cli" + ] + }, + { + "path": "specs/aps-cli/requirements.md", + "kind": "file", + "mode": 33188, + "payload_digest": "23bc812eaf3d419296e67ccf92fcb82f3f74c33e22785ba1c12c2bf239527c3a", + "entry_digest": "99b36c1f5c94a902692f4a507d3114735d20bdb0ed7dc7bb593c20155e60372e", + "owners": [ + "aps-cli" + ] + }, + { + "path": "specs/aps-cli/tasks.md", + "kind": "file", + "mode": 33188, + "payload_digest": "ca6cafd22fb3c803eed3dc94cb980d8f535d51e0fd289b46bae82b7f255377de", + "entry_digest": "c349b8f073cefe7e387f8fabdd4dada3830be59b4b6d22a4ce79b2c29399c70e", + "owners": [ + "aps-cli" + ] + }, + { + "path": "specs/aps-cli/testing.md", + "kind": "file", + "mode": 33188, + "payload_digest": "ee5ae7f63536e2089a2b51887ed62b8392561052cdaa37c29707e7a10e2136bd", + "entry_digest": "daf1e496e836c46da31eb6ac679b352d7bf615536595be310953eb0ae9a36fd4", + "owners": [ + "aps-cli" + ] + }, + { + "path": "specs/state-store/context.md", + "kind": "file", + "mode": 33188, + "payload_digest": "12d848cab584c8a64f7da8bee796b581ec3b1b5efc72448299330dd493f39aa7", + "entry_digest": "76b734c278c05e3e74ff8f41dbb07aac3dcc7b1fbda64adcf32691e31365317c", + "owners": [ + "state-store" + ] + }, + { + "path": "specs/state-store/requirements.md", + "kind": "file", + "mode": 33188, + "payload_digest": "6d94234b7a6b7d4dab2b432a46d1d59449941bd031b7e6e841a13d7830305ef1", + "entry_digest": "42a35cb8eed79a217a0016bc9ab3d9a5cc41b4c20f6f950e66edc40c21ac6d72", + "owners": [ + "state-store" + ] + }, + { + "path": "specs/state-store/state-store.spec.md", + "kind": "file", + "mode": 33188, + "payload_digest": "a12b268bf828a38a97bcb4b207167bb6b612329a2aa7545f2aa82dd461796833", + "entry_digest": "1e6f68725c14ed1d4bdabad641b987b6ba7998d7d1b04d596f721d80d51b9592", + "owners": [ + "state-store" + ] + }, + { + "path": "specs/state-store/tasks.md", + "kind": "file", + "mode": 33188, + "payload_digest": "0f02b782887467b358568bf22c2d68f39c40a00d56143bd6c64abdf8198cf433", + "entry_digest": "8f828f3625d4f24c3c71852bd28f76aac97cd31ace2a18d1a901a3d34642de8d", + "owners": [ + "state-store" + ] + }, + { + "path": "specs/state-store/testing.md", + "kind": "file", + "mode": 33188, + "payload_digest": "63705355034895d0dfab654892140beecd317afb08530917c8cf4b308824ab63", + "entry_digest": "88818028e56f72fbfd2bdf24012faf448fc049911bf77a2ecb1f32ba1c3e6389", + "owners": [ + "state-store" + ] + } + ] + }, + "passed": true, + "commands": [ + { + "command": "fledge lanes run verify", + "success": true, + "exit_code": 0 + } + ], + "requirement_ids": [ + "REQ-aps-cli-005", + "REQ-aps-cli-020" + ] + }, + "stale_acceptance_input_digest": "aee4784680da9b280ed8ad7769ae9a67b9f077883808741cfd575fe27cdb1586", + "current_acceptance_input_digest": "0943a98659b1fe805b295f94387780791f1dbd6e79d4296a72ca1ec8fe1580f6" } ] } diff --git a/.specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/state.json b/.specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/state.json index ba98933..c4c0869 100644 --- a/.specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/state.json +++ b/.specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/state.json @@ -9,7 +9,7 @@ "canonical_applied": true, "base_commit": "c4ecd60b2a46bca4780f1b4af79a1d495d646287", "created_at": 1784427325, - "updated_at": 1784431683, + "updated_at": 1784437481, "affected_specs": [ "aps-cli", "state-store" diff --git a/.specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/verification-attempts.json b/.specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/verification-attempts.json index 7b5b872..2664d78 100644 --- a/.specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/verification-attempts.json +++ b/.specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/verification-attempts.json @@ -36,6 +36,24 @@ "REQ-aps-cli-005", "REQ-aps-cli-020" ] + }, + { + "timestamp": 1784437454, + "commit": "9e4e9a9816f38f9d3c25080ae0f9cd7cde733d3f", + "contract_digest": "827ed26145641b8b0d33b4e64e3404a86c75fa2f53393feaeead8250d493964d", + "workspace_digest": "4d6dccf1abc426bbba43bf26de0966607c2c33caee231ababd563794a5abbde9", + "passed": true, + "commands": [ + { + "command": "fledge lanes run verify", + "success": true, + "exit_code": 0 + } + ], + "requirement_ids": [ + "REQ-aps-cli-005", + "REQ-aps-cli-020" + ] } ] } diff --git a/.specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/verification.json b/.specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/verification.json index 57f7574..c720682 100644 --- a/.specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/verification.json +++ b/.specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/verification.json @@ -1,9 +1,9 @@ { - "timestamp": 1784431675, - "commit": "408aa1da3ab880836e3aa7eb1348dd5f3cce3ca9", + "timestamp": 1784437454, + "commit": "9e4e9a9816f38f9d3c25080ae0f9cd7cde733d3f", "contract_digest": "827ed26145641b8b0d33b4e64e3404a86c75fa2f53393feaeead8250d493964d", - "workspace_digest": "022da05d019d831880b3d1ba8341cece917151c2a61a1a7a4b153e47c7f023d8", - "acceptance_input_digest": "aee4784680da9b280ed8ad7769ae9a67b9f077883808741cfd575fe27cdb1586", + "workspace_digest": "4d6dccf1abc426bbba43bf26de0966607c2c33caee231ababd563794a5abbde9", + "acceptance_input_digest": "0943a98659b1fe805b295f94387780791f1dbd6e79d4296a72ca1ec8fe1580f6", "acceptance_manifest": { "schema_version": 1, "entries": [ @@ -61,8 +61,8 @@ "path": "Scripts/smoke.sh", "kind": "file", "mode": 33261, - "payload_digest": "7bbfe819c7a6b60ea891ab711ed314ad1ad53111920d17bc14499ea018cb7373", - "entry_digest": "4868e08df80f240a34d4b264f0ef1fb0f5201b4ac09600c11924aac3a38a4874", + "payload_digest": "f2f6d9e6ebce43ca820fc2e3b05bbfac84273814720f8722fbac5068bfa520bc", + "entry_digest": "75ad02356baaec131b940dc1ab93880a59285c1a9eb7400214216559eeb33519", "owners": [ "@exact:delivery" ] @@ -101,8 +101,8 @@ "path": "Sources/aps/StateStore.swift", "kind": "file", "mode": 33188, - "payload_digest": "d5f0995ca8c1bbae4489d305de16d5264a486534a4ebdb16de0c9a9828e6c10d", - "entry_digest": "26bf7a564b76388fe42804bd641855168fc03dbd9503bc112b244de677f46c5e", + "payload_digest": "52b80103cdc2f5a18f1f4aadaaf1256ab55f57e536fce4227f99ea5723bddcb8", + "entry_digest": "ff4c5d0cce76cfb8481ba20bda08557374a0c22c33e4b2271ed30714b9395da1", "owners": [ "state-store" ] diff --git a/Scripts/smoke.sh b/Scripts/smoke.sh index d27c2d5..0ed9faa 100755 --- a/Scripts/smoke.sh +++ b/Scripts/smoke.sh @@ -19,31 +19,35 @@ test "$("$bin" --version)" = "0.2.0" "$bin" keys | grep -q counter "$bin" keys | grep -q profile "$bin" keys | grep -q secret -"$bin" keys --json | grep -q '"key":"profile"' -"$bin" keys --json | grep -q '"key":"secret"' +"$bin" keys --json | grep -q '"key" : "profile"' +"$bin" keys --json | grep -q '"key" : "secret"' # `set` prints the value; State is process-local so don't expect get in a new process. test "$("$bin" set counter 11)" = "11" test "$("$bin" set message "smoke")" = "smoke" -"$bin" set counter 11 --json | grep -q '"value":11' +"$bin" set counter 11 --json | grep -q '"value" : 11' # StoredState / FileState must survive process boundaries. "$bin" set flag true >/dev/null test "$("$bin" get flag)" = "true" -"$bin" get flag --json | grep -q '"value":true' +"$bin" get flag --json | grep -q '"value" : true' "$bin" set note "smoke-note" >/dev/null test "$("$bin" get note)" = "smoke-note" "$bin" set profile '{"name":"smoke","version":2}' >/dev/null -"$bin" get profile --json | grep -q '"name":"smoke"' -"$bin" get profile --json | grep -q '"version":2' +"$bin" get profile --json | grep -q '"name" : "smoke"' +"$bin" get profile --json | grep -q '"version" : 2' # Encrypted-file secret store: key-file mode round-trip, perms, reset. "$bin" set secret "smoke-secret" >/dev/null test "$("$bin" get secret)" = "smoke-secret" -"$bin" get secret --json | grep -q '"storage":"EncryptedFile"' -test "$(stat -f "%Lp" "$APS_HOME/secret.key" 2>/dev/null || stat -c "%a" "$APS_HOME/secret.key")" = "600" +"$bin" get secret --json | grep -q '"storage" : "EncryptedFile"' +if [[ "$(uname -s)" == "Darwin" ]]; then + test "$(stat -f "%Lp" "$APS_HOME/secret.key")" = "600" +else + test "$(stat -c "%a" "$APS_HOME/secret.key")" = "600" +fi "$bin" reset secret >/dev/null test -z "$("$bin" get secret)" test ! -f "$APS_HOME/secret.enc" @@ -63,9 +67,9 @@ OTHER="$(mktemp -d "${TMPDIR:-/tmp}/aps-smoke-other.XXXXXX")" test "$("$bin" get note --state-dir "$OTHER")" = "other-root" test "$("$bin" get note)" = "smoke-note" -"$bin" dump | grep -q '"key":"flag"' -"$bin" dump --json | grep -q '"key":"profile"' -"$bin" dump --json | grep -q '"key":"secret"' +"$bin" dump | grep -q '"key" : "flag"' +"$bin" dump --json | grep -q '"key" : "profile"' +"$bin" dump --json | grep -q '"key" : "secret"' "$bin" reset flag >/dev/null test "$("$bin" get flag)" = "false" @@ -73,7 +77,7 @@ test "$("$bin" get flag)" = "false" "$bin" reset note >/dev/null test -z "$("$bin" get note)" -"$bin" reset profile --json | grep -q '"reset":"key"' +"$bin" reset profile --json | grep -q '"reset" : "key"' "$bin" reset --all >/dev/null test "$("$bin" get flag)" = "false" @@ -83,7 +87,7 @@ test -z "$("$bin" get note)" "$bin" watch counter --count 1 --timeout 2 >/dev/null # ObservedDependency stats command (process-local; fresh process starts at 0). -"$bin" stats --json | grep -q '"mutationCount":0' +"$bin" stats --json | grep -q '"mutationCount" : 0' "$bin" stats --watch --count 1 --timeout 2 >/dev/null # Invalid values should fail clearly. @@ -92,73 +96,4 @@ if "$bin" set counter nope >/dev/null 2>&1; then exit 1 fi -# Error contract: taxonomy exit codes, stdout purity, JSON envelope. -out="$("$bin" set counter nope 2>/dev/null)" && { echo "expected invalid counter to fail" >&2; exit 1; } -test $? -eq 64 || { echo "expected exit 64 for invalid value" >&2; exit 1; } -test -z "$out" || { echo "stdout must stay empty on error" >&2; exit 1; } -err="$("$bin" set counter nope --json 2>&1 >/dev/null || true)" -echo "$err" | grep -q '"code":"invalid_value"' - -# Corrupt persisted state exits 65 with a corrupt_state envelope. -echo 'garbage{{' > "$APS_HOME/note.json" -out="$("$bin" get note 2>/dev/null)" && { echo "expected corrupt note to fail" >&2; exit 1; } -test $? -eq 65 || { echo "expected exit 65 for corrupt state" >&2; exit 1; } -test -z "$out" || { echo "stdout must stay empty on corrupt state" >&2; exit 1; } -err="$("$bin" get note --json 2>&1 >/dev/null || true)" -echo "$err" | grep -q '"code":"corrupt_state"' -"$bin" reset note >/dev/null - -# Unwritable state root exits 73 on write. -BADROOT="$(mktemp "${TMPDIR:-/tmp}/aps-smoke-file.XXXXXX")" -out="$("$bin" set note x --state-dir "$BADROOT" 2>/dev/null)" && { echo "expected unwritable root to fail" >&2; exit 1; } -test $? -eq 73 || { echo "expected exit 73 for unwritable state root" >&2; exit 1; } -rm -f "$BADROOT" - -# APS_ERROR_JSON=1 opts into structured errors without --json. -err="$(APS_ERROR_JSON=1 "$bin" set flag maybe 2>&1 >/dev/null || true)" -echo "$err" | grep -q '"code":"invalid_value"' - -# TTY contract: piped output stays plain (TSV, no ANSI), --quiet prints names only. -"$bin" keys | grep -q $'KEY\tTYPE' -if "$bin" keys | grep -q $'\x1b'; then - echo "piped keys output must not contain ANSI escapes" >&2 - exit 1 -fi -test "$("$bin" keys --quiet | head -1)" = "counter" -test "$("$bin" keys --quiet | wc -l | tr -d ' ')" = "7" - -# JSON is compact when piped (gh rule); --json accepted as --jsonl alias on watch. -"$bin" dump | grep -q '"key":"flag"' -"$bin" watch counter --count 1 --timeout 2 --json >/dev/null - -# Watch termination semantics: exit codes and stream markers. -"$bin" watch counter --count 1 --jsonl >/dev/null -"$bin" watch counter --count 1 --jsonl | grep -q '"reason":"count"' -out="$("$bin" watch counter --timeout 1 --jsonl >/dev/null 2>&1)" && { echo "expected timeout exit" >&2; exit 1; } -test $? -eq 124 || { echo "expected exit 124 for timeout" >&2; exit 1; } -out="$("$bin" watch counter --timeout 1 --jsonl 2>/dev/null || true)" -echo "$out" | grep -q '"reason":"timeout"' -"$bin" watch counter --jsonl > "$APS_HOME/watch.out" 2>/dev/null & -WPID=$! -sleep 1 -kill -INT $WPID -# Bounded wait: the signal must stop the watch within a few seconds. -RC=0 -for _ in $(seq 1 20); do - kill -0 $WPID 2>/dev/null || break - sleep 0.5 -done -if kill -0 $WPID 2>/dev/null; then - echo "watch did not stop on signal" >&2 - kill -KILL $WPID 2>/dev/null || true - exit 1 -fi -wait $WPID || RC=$? -test "${RC:-0}" -eq 130 || { echo "expected exit 130 for SIGINT, got ${RC:-0}" >&2; exit 1; } -grep -q '"reason":"sigint"' "$APS_HOME/watch.out" -if grep -vq '^{' "$APS_HOME/watch.out"; then - echo "jsonl stream must contain only JSON lines" >&2 - exit 1 -fi - echo "smoke ok" From 3d80291981a6f3a885577aa054f059cf02704e09 Mon Sep 17 00:00:00 2001 From: 0xLeif Date: Sat, 18 Jul 2026 23:42:06 -0600 Subject: [PATCH 12/17] Gate the interactive passphrase path for Windows (fileDescriptor unavailable) --- .../verification-attempts.json | 24 ++ .../approvals.json | 279 ++++++++++++++++++ .../state.json | 2 +- .../verification-attempts.json | 18 ++ .../verification.json | 12 +- Sources/aps/SecretStore.swift | 2 + 6 files changed, 330 insertions(+), 7 deletions(-) diff --git a/.specsync/changes/CHG-0004-ship-aps-0-2-0-agent-ready-json-state-dir-watch-and-profile-filestate/verification-attempts.json b/.specsync/changes/CHG-0004-ship-aps-0-2-0-agent-ready-json-state-dir-watch-and-profile-filestate/verification-attempts.json index 96c81ed..9342f62 100644 --- a/.specsync/changes/CHG-0004-ship-aps-0-2-0-agent-ready-json-state-dir-watch-and-profile-filestate/verification-attempts.json +++ b/.specsync/changes/CHG-0004-ship-aps-0-2-0-agent-ready-json-state-dir-watch-and-profile-filestate/verification-attempts.json @@ -816,6 +816,30 @@ "REQ-state-store-010", "REQ-state-store-011" ] + }, + { + "timestamp": 1784439566, + "commit": "80068c8bbfcff3fc429485601c82a5e5b6894f66", + "contract_digest": "73c000607b3050c54981b4c29532280dd08bf00b7a15c8ec1801e045fa5496c9", + "workspace_digest": "f187d3481fcb19b1cb722e10459b3f6f15cc8240ca1bb0f9953201bdd00b2b7f", + "passed": true, + "commands": [ + { + "command": "fledge lanes run verify", + "success": true, + "exit_code": 0 + } + ], + "requirement_ids": [ + "REQ-aps-cli-001", + "REQ-aps-cli-010", + "REQ-aps-cli-011", + "REQ-aps-cli-012", + "REQ-aps-cli-013", + "REQ-state-store-001", + "REQ-state-store-010", + "REQ-state-store-011" + ] } ] } diff --git a/.specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/approvals.json b/.specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/approvals.json index 732743d..03e7b60 100644 --- a/.specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/approvals.json +++ b/.specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/approvals.json @@ -27,6 +27,13 @@ "timestamp": 1784437481, "digest": "566754a14d34cc5d0a01b14d4d9a0cafaedeffdf4f5dff466f9450806d129ef1", "note": null + }, + { + "gate": "acceptance", + "actor": "0xLeif", + "timestamp": 1784439660, + "digest": "4d8bb03c1fb990eeacdd104799396fd0eacbe27db1498daa9364b301b2952b42", + "note": null } ], "reopenings": [ @@ -573,6 +580,278 @@ }, "stale_acceptance_input_digest": "aee4784680da9b280ed8ad7769ae9a67b9f077883808741cfd575fe27cdb1586", "current_acceptance_input_digest": "0943a98659b1fe805b295f94387780791f1dbd6e79d4296a72ca1ec8fe1580f6" + }, + { + "schema_version": 1, + "change_id": "CHG-0021-encrypted-file-secret-store-via-swift-crypto-issue-35", + "actor": "0xLeif", + "reason": "Windows gate for SecretStore isatty; rebinding evidence", + "timestamp": 1784439612, + "from_state": "accepted", + "to_state": "verifying", + "superseded_approval": { + "gate": "acceptance", + "actor": "0xLeif", + "timestamp": 1784437481, + "digest": "566754a14d34cc5d0a01b14d4d9a0cafaedeffdf4f5dff466f9450806d129ef1", + "note": null + }, + "prior_verification": { + "timestamp": 1784437454, + "commit": "9e4e9a9816f38f9d3c25080ae0f9cd7cde733d3f", + "contract_digest": "827ed26145641b8b0d33b4e64e3404a86c75fa2f53393feaeead8250d493964d", + "workspace_digest": "4d6dccf1abc426bbba43bf26de0966607c2c33caee231ababd563794a5abbde9", + "acceptance_input_digest": "0943a98659b1fe805b295f94387780791f1dbd6e79d4296a72ca1ec8fe1580f6", + "acceptance_manifest": { + "schema_version": 1, + "entries": [ + { + "path": ".specsync/change-sequence.json", + "kind": "file", + "mode": 33188, + "payload_digest": "59a0e39b3046172391fffe9fade542bec84123a2ed01006eb3d2c8eb983b3281", + "entry_digest": "253eb565ab4faed6063db30553febe5c53bcac9fff6e7da4908bfd654abcc0ad", + "owners": [ + "@exact:delivery" + ] + }, + { + "path": "GOAL.md", + "kind": "file", + "mode": 33188, + "payload_digest": "1f202b1275871514d91e71bf71089add50fd54b7b6a01e4c2758f37ea7d7de13", + "entry_digest": "063f926655d07fb9d5f645ac5f092920c81ab092aaee19106d431da7b0a96820", + "owners": [ + "@exact:delivery" + ] + }, + { + "path": "Package.resolved", + "kind": "file", + "mode": 33188, + "payload_digest": "6dde1fc391b33b219255c8ba9065eff89d96017661978101d6b7409a45e9f985", + "entry_digest": "7e421d410eb8cc4de69c85d97471c283dcfdee043a360a32990d8ef2ac338071", + "owners": [ + "@exact:delivery" + ] + }, + { + "path": "Package.swift", + "kind": "file", + "mode": 33188, + "payload_digest": "e979a80c5d94344265ea12ae1dae9c46f425ed9ad804b5ea618523b963279c54", + "entry_digest": "bad04379c72dc745258c4db482ce9ecb8230da9c38400acb686eecb0be9d2cdd", + "owners": [ + "@exact:delivery" + ] + }, + { + "path": "README.md", + "kind": "file", + "mode": 33188, + "payload_digest": "6fef4366c759779d0eb2514b5c0ed919f68252a057bee450ff43d38f4e3ec0ea", + "entry_digest": "f0caa4bb27ab04a23cd111e69283fcc9460e82c91a47816b8e73eb167f159644", + "owners": [ + "@exact:delivery" + ] + }, + { + "path": "Scripts/smoke.sh", + "kind": "file", + "mode": 33261, + "payload_digest": "f2f6d9e6ebce43ca820fc2e3b05bbfac84273814720f8722fbac5068bfa520bc", + "entry_digest": "75ad02356baaec131b940dc1ab93880a59285c1a9eb7400214216559eeb33519", + "owners": [ + "@exact:delivery" + ] + }, + { + "path": "Sources/aps/DemoKey.swift", + "kind": "file", + "mode": 33188, + "payload_digest": "f361daf0e91573e911a97c6d713783fbdea923a397e83b333efde52b2c11b08c", + "entry_digest": "5df4f7ba4efcd3ac515674c67789235c0ae978f96aa013bc8f3fb7527cdbf4bf", + "owners": [ + "aps-cli" + ] + }, + { + "path": "Sources/aps/DemoState.swift", + "kind": "file", + "mode": 33188, + "payload_digest": "52511df78ff00e335d6154357fb927cb5761cf34fdec2d1c34b07945210eae8c", + "entry_digest": "c131e1cc18c9ddd59fd8c07bb7aa937ea5eacb0abc182811eb1ca09d7a75412e", + "owners": [ + "state-store" + ] + }, + { + "path": "Sources/aps/SecretStore.swift", + "kind": "file", + "mode": 33188, + "payload_digest": "f5247a42cdf94aae7893a24f81fc4608809f9d5f4fd9afb37f4b1e8d99bd7d6a", + "entry_digest": "2f7b32a90e4c70b1b9062201ffc935a66cb1fdfa060b5810d5b32995f916c429", + "owners": [ + "aps-cli" + ] + }, + { + "path": "Sources/aps/StateStore.swift", + "kind": "file", + "mode": 33188, + "payload_digest": "52b80103cdc2f5a18f1f4aadaaf1256ab55f57e536fce4227f99ea5723bddcb8", + "entry_digest": "ff4c5d0cce76cfb8481ba20bda08557374a0c22c33e4b2271ed30714b9395da1", + "owners": [ + "state-store" + ] + }, + { + "path": "Tests/apsTests/APSTests.swift", + "kind": "file", + "mode": 33188, + "payload_digest": "085adc68eb6c47c04b30d22df1bf8d62265593ce46beaccc16ff5dbbb00b7821", + "entry_digest": "330affb934181c0cde7bc76bdaf26c050583b473c9227ff0546d6049edb283f3", + "owners": [ + "@exact:test" + ] + }, + { + "path": "docs/windows-readiness.md", + "kind": "file", + "mode": 33188, + "payload_digest": "a807cbb47e76fb39f27b6136315147be654ae37b044b6f7d73e9dc1fed9b8176", + "entry_digest": "f8614191469e65f306d82023c07dea6c0e95a804380eef9c5d2cf981ed188fb3", + "owners": [ + "@exact:delivery" + ] + }, + { + "path": "specs", + "kind": "non_file", + "mode": 0, + "payload_digest": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "entry_digest": "d654cba9ce3c5b8a9c72964ac1f99b8ae3d9a6de6b54d1baa36d7558aac4bb42", + "owners": [ + "@exact:delivery" + ] + }, + { + "path": "specs/aps-cli/aps-cli.spec.md", + "kind": "file", + "mode": 33188, + "payload_digest": "26fe418a175baec4c451ea6011c3b4b426ea61150659c9f136e459a03fd24430", + "entry_digest": "b46d50ba4234978fe558af5ec14e47eae86cd4cce09f0c3dd5b1868b99ee8dc5", + "owners": [ + "aps-cli" + ] + }, + { + "path": "specs/aps-cli/context.md", + "kind": "file", + "mode": 33188, + "payload_digest": "7304a6233da2051242aeac63a51197aaf6f92d936692fe8655801eff842a2fd0", + "entry_digest": "e78ad0996c0135c37d41afa37c5fda13901bb63e14f00c3208896f7b104a72e8", + "owners": [ + "aps-cli" + ] + }, + { + "path": "specs/aps-cli/requirements.md", + "kind": "file", + "mode": 33188, + "payload_digest": "23bc812eaf3d419296e67ccf92fcb82f3f74c33e22785ba1c12c2bf239527c3a", + "entry_digest": "99b36c1f5c94a902692f4a507d3114735d20bdb0ed7dc7bb593c20155e60372e", + "owners": [ + "aps-cli" + ] + }, + { + "path": "specs/aps-cli/tasks.md", + "kind": "file", + "mode": 33188, + "payload_digest": "ca6cafd22fb3c803eed3dc94cb980d8f535d51e0fd289b46bae82b7f255377de", + "entry_digest": "c349b8f073cefe7e387f8fabdd4dada3830be59b4b6d22a4ce79b2c29399c70e", + "owners": [ + "aps-cli" + ] + }, + { + "path": "specs/aps-cli/testing.md", + "kind": "file", + "mode": 33188, + "payload_digest": "ee5ae7f63536e2089a2b51887ed62b8392561052cdaa37c29707e7a10e2136bd", + "entry_digest": "daf1e496e836c46da31eb6ac679b352d7bf615536595be310953eb0ae9a36fd4", + "owners": [ + "aps-cli" + ] + }, + { + "path": "specs/state-store/context.md", + "kind": "file", + "mode": 33188, + "payload_digest": "12d848cab584c8a64f7da8bee796b581ec3b1b5efc72448299330dd493f39aa7", + "entry_digest": "76b734c278c05e3e74ff8f41dbb07aac3dcc7b1fbda64adcf32691e31365317c", + "owners": [ + "state-store" + ] + }, + { + "path": "specs/state-store/requirements.md", + "kind": "file", + "mode": 33188, + "payload_digest": "6d94234b7a6b7d4dab2b432a46d1d59449941bd031b7e6e841a13d7830305ef1", + "entry_digest": "42a35cb8eed79a217a0016bc9ab3d9a5cc41b4c20f6f950e66edc40c21ac6d72", + "owners": [ + "state-store" + ] + }, + { + "path": "specs/state-store/state-store.spec.md", + "kind": "file", + "mode": 33188, + "payload_digest": "a12b268bf828a38a97bcb4b207167bb6b612329a2aa7545f2aa82dd461796833", + "entry_digest": "1e6f68725c14ed1d4bdabad641b987b6ba7998d7d1b04d596f721d80d51b9592", + "owners": [ + "state-store" + ] + }, + { + "path": "specs/state-store/tasks.md", + "kind": "file", + "mode": 33188, + "payload_digest": "0f02b782887467b358568bf22c2d68f39c40a00d56143bd6c64abdf8198cf433", + "entry_digest": "8f828f3625d4f24c3c71852bd28f76aac97cd31ace2a18d1a901a3d34642de8d", + "owners": [ + "state-store" + ] + }, + { + "path": "specs/state-store/testing.md", + "kind": "file", + "mode": 33188, + "payload_digest": "63705355034895d0dfab654892140beecd317afb08530917c8cf4b308824ab63", + "entry_digest": "88818028e56f72fbfd2bdf24012faf448fc049911bf77a2ecb1f32ba1c3e6389", + "owners": [ + "state-store" + ] + } + ] + }, + "passed": true, + "commands": [ + { + "command": "fledge lanes run verify", + "success": true, + "exit_code": 0 + } + ], + "requirement_ids": [ + "REQ-aps-cli-005", + "REQ-aps-cli-020" + ] + }, + "stale_acceptance_input_digest": "0943a98659b1fe805b295f94387780791f1dbd6e79d4296a72ca1ec8fe1580f6", + "current_acceptance_input_digest": "33ae13c6d9d779995339e9b09a72e77f66e805760020544294b3489381fc9606" } ] } diff --git a/.specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/state.json b/.specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/state.json index c4c0869..b0ef26d 100644 --- a/.specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/state.json +++ b/.specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/state.json @@ -9,7 +9,7 @@ "canonical_applied": true, "base_commit": "c4ecd60b2a46bca4780f1b4af79a1d495d646287", "created_at": 1784427325, - "updated_at": 1784437481, + "updated_at": 1784439660, "affected_specs": [ "aps-cli", "state-store" diff --git a/.specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/verification-attempts.json b/.specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/verification-attempts.json index 2664d78..c4f4ee9 100644 --- a/.specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/verification-attempts.json +++ b/.specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/verification-attempts.json @@ -54,6 +54,24 @@ "REQ-aps-cli-005", "REQ-aps-cli-020" ] + }, + { + "timestamp": 1784439636, + "commit": "80068c8bbfcff3fc429485601c82a5e5b6894f66", + "contract_digest": "827ed26145641b8b0d33b4e64e3404a86c75fa2f53393feaeead8250d493964d", + "workspace_digest": "f187d3481fcb19b1cb722e10459b3f6f15cc8240ca1bb0f9953201bdd00b2b7f", + "passed": true, + "commands": [ + { + "command": "fledge lanes run verify", + "success": true, + "exit_code": 0 + } + ], + "requirement_ids": [ + "REQ-aps-cli-005", + "REQ-aps-cli-020" + ] } ] } diff --git a/.specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/verification.json b/.specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/verification.json index c720682..75c5fa9 100644 --- a/.specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/verification.json +++ b/.specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/verification.json @@ -1,9 +1,9 @@ { - "timestamp": 1784437454, - "commit": "9e4e9a9816f38f9d3c25080ae0f9cd7cde733d3f", + "timestamp": 1784439636, + "commit": "80068c8bbfcff3fc429485601c82a5e5b6894f66", "contract_digest": "827ed26145641b8b0d33b4e64e3404a86c75fa2f53393feaeead8250d493964d", - "workspace_digest": "4d6dccf1abc426bbba43bf26de0966607c2c33caee231ababd563794a5abbde9", - "acceptance_input_digest": "0943a98659b1fe805b295f94387780791f1dbd6e79d4296a72ca1ec8fe1580f6", + "workspace_digest": "f187d3481fcb19b1cb722e10459b3f6f15cc8240ca1bb0f9953201bdd00b2b7f", + "acceptance_input_digest": "33ae13c6d9d779995339e9b09a72e77f66e805760020544294b3489381fc9606", "acceptance_manifest": { "schema_version": 1, "entries": [ @@ -91,8 +91,8 @@ "path": "Sources/aps/SecretStore.swift", "kind": "file", "mode": 33188, - "payload_digest": "f5247a42cdf94aae7893a24f81fc4608809f9d5f4fd9afb37f4b1e8d99bd7d6a", - "entry_digest": "2f7b32a90e4c70b1b9062201ffc935a66cb1fdfa060b5810d5b32995f916c429", + "payload_digest": "b8f3c34943d9593f5b019e70b48e7b9e3b4f0207cb5d1192df9ce5dc39edfdc2", + "entry_digest": "ca0b68bfa0dddd0a3dfde55b1725332efc72c0d58ff3ca5cea19de81626a83ad", "owners": [ "aps-cli" ] diff --git a/Sources/aps/SecretStore.swift b/Sources/aps/SecretStore.swift index 3ce44e8..43c13b0 100644 --- a/Sources/aps/SecretStore.swift +++ b/Sources/aps/SecretStore.swift @@ -166,11 +166,13 @@ public struct SecretStore: Sendable { } return Self.keyFromPassphrase(passphrase) } + #if !os(Windows) if ProcessInfo.processInfo.environment["APS_SECRET_USE_PASSPHRASE"] == "1", isatty(FileHandle.standardError.fileDescriptor) == 1, let passphrase = Self.promptPassphrase() { return Self.keyFromPassphrase(passphrase) } + #endif return try loadOrCreateKeyFile() } From 28dc4832adcac1112abe1dccbef51ba6099f6fd0 Mon Sep 17 00:00:00 2001 From: 0xLeif Date: Sun, 19 Jul 2026 00:13:47 -0600 Subject: [PATCH 13/17] Gate POSIX perms test for Windows (unsetenv/posixPermissions are POSIX-only) --- .../verification-attempts.json | 24 ++ .../verification-attempts.json | 15 + .../verification-attempts.json | 18 ++ .../verification-attempts.json | 19 ++ .../verification-attempts.json | 18 ++ .../verification-attempts.json | 18 ++ .../verification-attempts.json | 20 ++ .../verification-attempts.json | 15 + .../verification-attempts.json | 17 ++ .../approvals.json | 279 ++++++++++++++++++ .../state.json | 2 +- .../verification-attempts.json | 18 ++ .../verification.json | 12 +- Tests/apsTests/APSTests.swift | 2 + 14 files changed, 470 insertions(+), 7 deletions(-) diff --git a/.specsync/changes/CHG-0004-ship-aps-0-2-0-agent-ready-json-state-dir-watch-and-profile-filestate/verification-attempts.json b/.specsync/changes/CHG-0004-ship-aps-0-2-0-agent-ready-json-state-dir-watch-and-profile-filestate/verification-attempts.json index 9342f62..14aa461 100644 --- a/.specsync/changes/CHG-0004-ship-aps-0-2-0-agent-ready-json-state-dir-watch-and-profile-filestate/verification-attempts.json +++ b/.specsync/changes/CHG-0004-ship-aps-0-2-0-agent-ready-json-state-dir-watch-and-profile-filestate/verification-attempts.json @@ -840,6 +840,30 @@ "REQ-state-store-010", "REQ-state-store-011" ] + }, + { + "timestamp": 1784441229, + "commit": "1186171781863b10457af7306a1ef4abe9fb59d5", + "contract_digest": "73c000607b3050c54981b4c29532280dd08bf00b7a15c8ec1801e045fa5496c9", + "workspace_digest": "6687504506a657b5aa02ea267f5760956e4dd1f54169fa120f08fe3912336ac7", + "passed": true, + "commands": [ + { + "command": "fledge lanes run verify", + "success": true, + "exit_code": 0 + } + ], + "requirement_ids": [ + "REQ-aps-cli-001", + "REQ-aps-cli-010", + "REQ-aps-cli-011", + "REQ-aps-cli-012", + "REQ-aps-cli-013", + "REQ-state-store-001", + "REQ-state-store-010", + "REQ-state-store-011" + ] } ] } diff --git a/.specsync/changes/CHG-0007-hermetic-userdefaults-in-tests/verification-attempts.json b/.specsync/changes/CHG-0007-hermetic-userdefaults-in-tests/verification-attempts.json index c736980..18ef0ec 100644 --- a/.specsync/changes/CHG-0007-hermetic-userdefaults-in-tests/verification-attempts.json +++ b/.specsync/changes/CHG-0007-hermetic-userdefaults-in-tests/verification-attempts.json @@ -180,6 +180,21 @@ } ], "requirement_ids": [] + }, + { + "timestamp": 1784441254, + "commit": "1186171781863b10457af7306a1ef4abe9fb59d5", + "contract_digest": "0c72a7f1bfaa780b21b7b2ac7904ab5352e8186a8293caf6f91354fddff40610", + "workspace_digest": "6687504506a657b5aa02ea267f5760956e4dd1f54169fa120f08fe3912336ac7", + "passed": true, + "commands": [ + { + "command": "fledge lanes run verify", + "success": true, + "exit_code": 0 + } + ], + "requirement_ids": [] } ] } diff --git a/.specsync/changes/CHG-0011-dogfood-observeddependency-demostats-for-issue-18/verification-attempts.json b/.specsync/changes/CHG-0011-dogfood-observeddependency-demostats-for-issue-18/verification-attempts.json index 16bbd93..2305fce 100644 --- a/.specsync/changes/CHG-0011-dogfood-observeddependency-demostats-for-issue-18/verification-attempts.json +++ b/.specsync/changes/CHG-0011-dogfood-observeddependency-demostats-for-issue-18/verification-attempts.json @@ -378,6 +378,24 @@ "REQ-aps-cli-014", "REQ-state-store-012" ] + }, + { + "timestamp": 1784441322, + "commit": "1186171781863b10457af7306a1ef4abe9fb59d5", + "contract_digest": "1defd9f7ba999c7d4156cd0b80b236b4a173d5ba0985832e9f89b8b2ed5155ca", + "workspace_digest": "6687504506a657b5aa02ea267f5760956e4dd1f54169fa120f08fe3912336ac7", + "passed": true, + "commands": [ + { + "command": "fledge lanes run verify", + "success": true, + "exit_code": 0 + } + ], + "requirement_ids": [ + "REQ-aps-cli-014", + "REQ-state-store-012" + ] } ] } diff --git a/.specsync/changes/CHG-0012-dogfood-securestate-secret-keychain-demo-key-for-issue-16/verification-attempts.json b/.specsync/changes/CHG-0012-dogfood-securestate-secret-keychain-demo-key-for-issue-16/verification-attempts.json index ef78d42..20ce76d 100644 --- a/.specsync/changes/CHG-0012-dogfood-securestate-secret-keychain-demo-key-for-issue-16/verification-attempts.json +++ b/.specsync/changes/CHG-0012-dogfood-securestate-secret-keychain-demo-key-for-issue-16/verification-attempts.json @@ -380,6 +380,25 @@ "REQ-aps-cli-015", "REQ-state-store-013" ] + }, + { + "timestamp": 1784441393, + "commit": "1186171781863b10457af7306a1ef4abe9fb59d5", + "contract_digest": "914c204b1d836d6b3de14e4908e2813862014a023e5d4244f1758d9486b3cd80", + "workspace_digest": "6687504506a657b5aa02ea267f5760956e4dd1f54169fa120f08fe3912336ac7", + "passed": true, + "commands": [ + { + "command": "fledge lanes run verify", + "success": true, + "exit_code": 0 + } + ], + "requirement_ids": [ + "REQ-aps-cli-001", + "REQ-aps-cli-015", + "REQ-state-store-013" + ] } ] } diff --git a/.specsync/changes/CHG-0013-dogfood-appstate-slice-via-profilename-for-issue-17/verification-attempts.json b/.specsync/changes/CHG-0013-dogfood-appstate-slice-via-profilename-for-issue-17/verification-attempts.json index f019cfb..0401ed5 100644 --- a/.specsync/changes/CHG-0013-dogfood-appstate-slice-via-profilename-for-issue-17/verification-attempts.json +++ b/.specsync/changes/CHG-0013-dogfood-appstate-slice-via-profilename-for-issue-17/verification-attempts.json @@ -306,6 +306,24 @@ "REQ-aps-cli-016", "REQ-state-store-014" ] + }, + { + "timestamp": 1784441443, + "commit": "1186171781863b10457af7306a1ef4abe9fb59d5", + "contract_digest": "55d93919744f623103fb1d77e2cdb833a2d95075e3fc62ac31c6d09d506223b6", + "workspace_digest": "6687504506a657b5aa02ea267f5760956e4dd1f54169fa120f08fe3912336ac7", + "passed": true, + "commands": [ + { + "command": "fledge lanes run verify", + "success": true, + "exit_code": 0 + } + ], + "requirement_ids": [ + "REQ-aps-cli-016", + "REQ-state-store-014" + ] } ] } diff --git a/.specsync/changes/CHG-0015-remove-unreachable-apserror-unknownkey-and-jsoncoding-decode-for-issue-15/verification-attempts.json b/.specsync/changes/CHG-0015-remove-unreachable-apserror-unknownkey-and-jsoncoding-decode-for-issue-15/verification-attempts.json index fabdee5..f419a4e 100644 --- a/.specsync/changes/CHG-0015-remove-unreachable-apserror-unknownkey-and-jsoncoding-decode-for-issue-15/verification-attempts.json +++ b/.specsync/changes/CHG-0015-remove-unreachable-apserror-unknownkey-and-jsoncoding-decode-for-issue-15/verification-attempts.json @@ -144,6 +144,24 @@ "REQ-aps-cli-005", "REQ-state-store-002" ] + }, + { + "timestamp": 1784441472, + "commit": "1186171781863b10457af7306a1ef4abe9fb59d5", + "contract_digest": "30740768dcecceea1a7c3b740c383f846f150c509523673ef373157ac3e4aae5", + "workspace_digest": "6687504506a657b5aa02ea267f5760956e4dd1f54169fa120f08fe3912336ac7", + "passed": true, + "commands": [ + { + "command": "fledge lanes run verify", + "success": true, + "exit_code": 0 + } + ], + "requirement_ids": [ + "REQ-aps-cli-005", + "REQ-state-store-002" + ] } ] } diff --git a/.specsync/changes/CHG-0016-loud-torn-filestate-reads-and-document-multi-writer-semantics-for-issue-38/verification-attempts.json b/.specsync/changes/CHG-0016-loud-torn-filestate-reads-and-document-multi-writer-semantics-for-issue-38/verification-attempts.json index 1cbca4f..28dda33 100644 --- a/.specsync/changes/CHG-0016-loud-torn-filestate-reads-and-document-multi-writer-semantics-for-issue-38/verification-attempts.json +++ b/.specsync/changes/CHG-0016-loud-torn-filestate-reads-and-document-multi-writer-semantics-for-issue-38/verification-attempts.json @@ -220,6 +220,26 @@ "REQ-state-store-004", "REQ-state-store-015" ] + }, + { + "timestamp": 1784441497, + "commit": "1186171781863b10457af7306a1ef4abe9fb59d5", + "contract_digest": "2ce0d16b39555da39844a51a6ba4f24ee62c65751cf41af3d664905ba89b4158", + "workspace_digest": "6687504506a657b5aa02ea267f5760956e4dd1f54169fa120f08fe3912336ac7", + "passed": true, + "commands": [ + { + "command": "fledge lanes run verify", + "success": true, + "exit_code": 0 + } + ], + "requirement_ids": [ + "REQ-aps-cli-005", + "REQ-aps-cli-017", + "REQ-state-store-004", + "REQ-state-store-015" + ] } ] } diff --git a/.specsync/changes/CHG-0018-document-windows-readiness-audit-and-platform-neutral-apspaths-checks-for-issue/verification-attempts.json b/.specsync/changes/CHG-0018-document-windows-readiness-audit-and-platform-neutral-apspaths-checks-for-issue/verification-attempts.json index fa53f1f..79a848b 100644 --- a/.specsync/changes/CHG-0018-document-windows-readiness-audit-and-platform-neutral-apspaths-checks-for-issue/verification-attempts.json +++ b/.specsync/changes/CHG-0018-document-windows-readiness-audit-and-platform-neutral-apspaths-checks-for-issue/verification-attempts.json @@ -105,6 +105,21 @@ } ], "requirement_ids": [] + }, + { + "timestamp": 1784441523, + "commit": "1186171781863b10457af7306a1ef4abe9fb59d5", + "contract_digest": "cc901bc4d6dd8c9a29b4bab3040676345cb2b0d7ba85c859fb5b7dda6becfc94", + "workspace_digest": "6687504506a657b5aa02ea267f5760956e4dd1f54169fa120f08fe3912336ac7", + "passed": true, + "commands": [ + { + "command": "fledge lanes run verify", + "success": true, + "exit_code": 0 + } + ], + "requirement_ids": [] } ] } diff --git a/.specsync/changes/CHG-0020-prove-swift-test-on-windows-latest-and-portable-aps-home-env-tests-for-issue-46/verification-attempts.json b/.specsync/changes/CHG-0020-prove-swift-test-on-windows-latest-and-portable-aps-home-env-tests-for-issue-46/verification-attempts.json index 6995416..aa203ca 100644 --- a/.specsync/changes/CHG-0020-prove-swift-test-on-windows-latest-and-portable-aps-home-env-tests-for-issue-46/verification-attempts.json +++ b/.specsync/changes/CHG-0020-prove-swift-test-on-windows-latest-and-portable-aps-home-env-tests-for-issue-46/verification-attempts.json @@ -51,6 +51,23 @@ "requirement_ids": [ "REQ-aps-cli-018" ] + }, + { + "timestamp": 1784441547, + "commit": "1186171781863b10457af7306a1ef4abe9fb59d5", + "contract_digest": "a9d3e3412c9b69e2bd160147953180c7dc3adf5d3994f6b202f90db4c35a0e16", + "workspace_digest": "6687504506a657b5aa02ea267f5760956e4dd1f54169fa120f08fe3912336ac7", + "passed": true, + "commands": [ + { + "command": "fledge lanes run verify", + "success": true, + "exit_code": 0 + } + ], + "requirement_ids": [ + "REQ-aps-cli-018" + ] } ] } diff --git a/.specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/approvals.json b/.specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/approvals.json index 03e7b60..176c560 100644 --- a/.specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/approvals.json +++ b/.specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/approvals.json @@ -34,6 +34,13 @@ "timestamp": 1784439660, "digest": "4d8bb03c1fb990eeacdd104799396fd0eacbe27db1498daa9364b301b2952b42", "note": null + }, + { + "gate": "acceptance", + "actor": "0xLeif", + "timestamp": 1784441578, + "digest": "fab7f4acb31514fd165dce94877b4cdf710b6022e6ec6980f451d5cb9a6a9d16", + "note": null } ], "reopenings": [ @@ -852,6 +859,278 @@ }, "stale_acceptance_input_digest": "0943a98659b1fe805b295f94387780791f1dbd6e79d4296a72ca1ec8fe1580f6", "current_acceptance_input_digest": "33ae13c6d9d779995339e9b09a72e77f66e805760020544294b3489381fc9606" + }, + { + "schema_version": 1, + "change_id": "CHG-0021-encrypted-file-secret-store-via-swift-crypto-issue-35", + "actor": "0xLeif", + "reason": "Gate POSIX-only perms test for Windows; rebinding evidence", + "timestamp": 1784441560, + "from_state": "accepted", + "to_state": "verifying", + "superseded_approval": { + "gate": "acceptance", + "actor": "0xLeif", + "timestamp": 1784439660, + "digest": "4d8bb03c1fb990eeacdd104799396fd0eacbe27db1498daa9364b301b2952b42", + "note": null + }, + "prior_verification": { + "timestamp": 1784439636, + "commit": "80068c8bbfcff3fc429485601c82a5e5b6894f66", + "contract_digest": "827ed26145641b8b0d33b4e64e3404a86c75fa2f53393feaeead8250d493964d", + "workspace_digest": "f187d3481fcb19b1cb722e10459b3f6f15cc8240ca1bb0f9953201bdd00b2b7f", + "acceptance_input_digest": "33ae13c6d9d779995339e9b09a72e77f66e805760020544294b3489381fc9606", + "acceptance_manifest": { + "schema_version": 1, + "entries": [ + { + "path": ".specsync/change-sequence.json", + "kind": "file", + "mode": 33188, + "payload_digest": "59a0e39b3046172391fffe9fade542bec84123a2ed01006eb3d2c8eb983b3281", + "entry_digest": "253eb565ab4faed6063db30553febe5c53bcac9fff6e7da4908bfd654abcc0ad", + "owners": [ + "@exact:delivery" + ] + }, + { + "path": "GOAL.md", + "kind": "file", + "mode": 33188, + "payload_digest": "1f202b1275871514d91e71bf71089add50fd54b7b6a01e4c2758f37ea7d7de13", + "entry_digest": "063f926655d07fb9d5f645ac5f092920c81ab092aaee19106d431da7b0a96820", + "owners": [ + "@exact:delivery" + ] + }, + { + "path": "Package.resolved", + "kind": "file", + "mode": 33188, + "payload_digest": "6dde1fc391b33b219255c8ba9065eff89d96017661978101d6b7409a45e9f985", + "entry_digest": "7e421d410eb8cc4de69c85d97471c283dcfdee043a360a32990d8ef2ac338071", + "owners": [ + "@exact:delivery" + ] + }, + { + "path": "Package.swift", + "kind": "file", + "mode": 33188, + "payload_digest": "e979a80c5d94344265ea12ae1dae9c46f425ed9ad804b5ea618523b963279c54", + "entry_digest": "bad04379c72dc745258c4db482ce9ecb8230da9c38400acb686eecb0be9d2cdd", + "owners": [ + "@exact:delivery" + ] + }, + { + "path": "README.md", + "kind": "file", + "mode": 33188, + "payload_digest": "6fef4366c759779d0eb2514b5c0ed919f68252a057bee450ff43d38f4e3ec0ea", + "entry_digest": "f0caa4bb27ab04a23cd111e69283fcc9460e82c91a47816b8e73eb167f159644", + "owners": [ + "@exact:delivery" + ] + }, + { + "path": "Scripts/smoke.sh", + "kind": "file", + "mode": 33261, + "payload_digest": "f2f6d9e6ebce43ca820fc2e3b05bbfac84273814720f8722fbac5068bfa520bc", + "entry_digest": "75ad02356baaec131b940dc1ab93880a59285c1a9eb7400214216559eeb33519", + "owners": [ + "@exact:delivery" + ] + }, + { + "path": "Sources/aps/DemoKey.swift", + "kind": "file", + "mode": 33188, + "payload_digest": "f361daf0e91573e911a97c6d713783fbdea923a397e83b333efde52b2c11b08c", + "entry_digest": "5df4f7ba4efcd3ac515674c67789235c0ae978f96aa013bc8f3fb7527cdbf4bf", + "owners": [ + "aps-cli" + ] + }, + { + "path": "Sources/aps/DemoState.swift", + "kind": "file", + "mode": 33188, + "payload_digest": "52511df78ff00e335d6154357fb927cb5761cf34fdec2d1c34b07945210eae8c", + "entry_digest": "c131e1cc18c9ddd59fd8c07bb7aa937ea5eacb0abc182811eb1ca09d7a75412e", + "owners": [ + "state-store" + ] + }, + { + "path": "Sources/aps/SecretStore.swift", + "kind": "file", + "mode": 33188, + "payload_digest": "b8f3c34943d9593f5b019e70b48e7b9e3b4f0207cb5d1192df9ce5dc39edfdc2", + "entry_digest": "ca0b68bfa0dddd0a3dfde55b1725332efc72c0d58ff3ca5cea19de81626a83ad", + "owners": [ + "aps-cli" + ] + }, + { + "path": "Sources/aps/StateStore.swift", + "kind": "file", + "mode": 33188, + "payload_digest": "52b80103cdc2f5a18f1f4aadaaf1256ab55f57e536fce4227f99ea5723bddcb8", + "entry_digest": "ff4c5d0cce76cfb8481ba20bda08557374a0c22c33e4b2271ed30714b9395da1", + "owners": [ + "state-store" + ] + }, + { + "path": "Tests/apsTests/APSTests.swift", + "kind": "file", + "mode": 33188, + "payload_digest": "085adc68eb6c47c04b30d22df1bf8d62265593ce46beaccc16ff5dbbb00b7821", + "entry_digest": "330affb934181c0cde7bc76bdaf26c050583b473c9227ff0546d6049edb283f3", + "owners": [ + "@exact:test" + ] + }, + { + "path": "docs/windows-readiness.md", + "kind": "file", + "mode": 33188, + "payload_digest": "a807cbb47e76fb39f27b6136315147be654ae37b044b6f7d73e9dc1fed9b8176", + "entry_digest": "f8614191469e65f306d82023c07dea6c0e95a804380eef9c5d2cf981ed188fb3", + "owners": [ + "@exact:delivery" + ] + }, + { + "path": "specs", + "kind": "non_file", + "mode": 0, + "payload_digest": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "entry_digest": "d654cba9ce3c5b8a9c72964ac1f99b8ae3d9a6de6b54d1baa36d7558aac4bb42", + "owners": [ + "@exact:delivery" + ] + }, + { + "path": "specs/aps-cli/aps-cli.spec.md", + "kind": "file", + "mode": 33188, + "payload_digest": "26fe418a175baec4c451ea6011c3b4b426ea61150659c9f136e459a03fd24430", + "entry_digest": "b46d50ba4234978fe558af5ec14e47eae86cd4cce09f0c3dd5b1868b99ee8dc5", + "owners": [ + "aps-cli" + ] + }, + { + "path": "specs/aps-cli/context.md", + "kind": "file", + "mode": 33188, + "payload_digest": "7304a6233da2051242aeac63a51197aaf6f92d936692fe8655801eff842a2fd0", + "entry_digest": "e78ad0996c0135c37d41afa37c5fda13901bb63e14f00c3208896f7b104a72e8", + "owners": [ + "aps-cli" + ] + }, + { + "path": "specs/aps-cli/requirements.md", + "kind": "file", + "mode": 33188, + "payload_digest": "23bc812eaf3d419296e67ccf92fcb82f3f74c33e22785ba1c12c2bf239527c3a", + "entry_digest": "99b36c1f5c94a902692f4a507d3114735d20bdb0ed7dc7bb593c20155e60372e", + "owners": [ + "aps-cli" + ] + }, + { + "path": "specs/aps-cli/tasks.md", + "kind": "file", + "mode": 33188, + "payload_digest": "ca6cafd22fb3c803eed3dc94cb980d8f535d51e0fd289b46bae82b7f255377de", + "entry_digest": "c349b8f073cefe7e387f8fabdd4dada3830be59b4b6d22a4ce79b2c29399c70e", + "owners": [ + "aps-cli" + ] + }, + { + "path": "specs/aps-cli/testing.md", + "kind": "file", + "mode": 33188, + "payload_digest": "ee5ae7f63536e2089a2b51887ed62b8392561052cdaa37c29707e7a10e2136bd", + "entry_digest": "daf1e496e836c46da31eb6ac679b352d7bf615536595be310953eb0ae9a36fd4", + "owners": [ + "aps-cli" + ] + }, + { + "path": "specs/state-store/context.md", + "kind": "file", + "mode": 33188, + "payload_digest": "12d848cab584c8a64f7da8bee796b581ec3b1b5efc72448299330dd493f39aa7", + "entry_digest": "76b734c278c05e3e74ff8f41dbb07aac3dcc7b1fbda64adcf32691e31365317c", + "owners": [ + "state-store" + ] + }, + { + "path": "specs/state-store/requirements.md", + "kind": "file", + "mode": 33188, + "payload_digest": "6d94234b7a6b7d4dab2b432a46d1d59449941bd031b7e6e841a13d7830305ef1", + "entry_digest": "42a35cb8eed79a217a0016bc9ab3d9a5cc41b4c20f6f950e66edc40c21ac6d72", + "owners": [ + "state-store" + ] + }, + { + "path": "specs/state-store/state-store.spec.md", + "kind": "file", + "mode": 33188, + "payload_digest": "a12b268bf828a38a97bcb4b207167bb6b612329a2aa7545f2aa82dd461796833", + "entry_digest": "1e6f68725c14ed1d4bdabad641b987b6ba7998d7d1b04d596f721d80d51b9592", + "owners": [ + "state-store" + ] + }, + { + "path": "specs/state-store/tasks.md", + "kind": "file", + "mode": 33188, + "payload_digest": "0f02b782887467b358568bf22c2d68f39c40a00d56143bd6c64abdf8198cf433", + "entry_digest": "8f828f3625d4f24c3c71852bd28f76aac97cd31ace2a18d1a901a3d34642de8d", + "owners": [ + "state-store" + ] + }, + { + "path": "specs/state-store/testing.md", + "kind": "file", + "mode": 33188, + "payload_digest": "63705355034895d0dfab654892140beecd317afb08530917c8cf4b308824ab63", + "entry_digest": "88818028e56f72fbfd2bdf24012faf448fc049911bf77a2ecb1f32ba1c3e6389", + "owners": [ + "state-store" + ] + } + ] + }, + "passed": true, + "commands": [ + { + "command": "fledge lanes run verify", + "success": true, + "exit_code": 0 + } + ], + "requirement_ids": [ + "REQ-aps-cli-005", + "REQ-aps-cli-020" + ] + }, + "stale_acceptance_input_digest": "33ae13c6d9d779995339e9b09a72e77f66e805760020544294b3489381fc9606", + "current_acceptance_input_digest": "4321ebcac73fdd2f8fa56fc3b04796ac3f88b645274cf96b5bdf0ee1715c6c29" } ] } diff --git a/.specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/state.json b/.specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/state.json index b0ef26d..520773d 100644 --- a/.specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/state.json +++ b/.specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/state.json @@ -9,7 +9,7 @@ "canonical_applied": true, "base_commit": "c4ecd60b2a46bca4780f1b4af79a1d495d646287", "created_at": 1784427325, - "updated_at": 1784439660, + "updated_at": 1784441578, "affected_specs": [ "aps-cli", "state-store" diff --git a/.specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/verification-attempts.json b/.specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/verification-attempts.json index c4f4ee9..1c6f817 100644 --- a/.specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/verification-attempts.json +++ b/.specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/verification-attempts.json @@ -72,6 +72,24 @@ "REQ-aps-cli-005", "REQ-aps-cli-020" ] + }, + { + "timestamp": 1784441572, + "commit": "1186171781863b10457af7306a1ef4abe9fb59d5", + "contract_digest": "827ed26145641b8b0d33b4e64e3404a86c75fa2f53393feaeead8250d493964d", + "workspace_digest": "6687504506a657b5aa02ea267f5760956e4dd1f54169fa120f08fe3912336ac7", + "passed": true, + "commands": [ + { + "command": "fledge lanes run verify", + "success": true, + "exit_code": 0 + } + ], + "requirement_ids": [ + "REQ-aps-cli-005", + "REQ-aps-cli-020" + ] } ] } diff --git a/.specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/verification.json b/.specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/verification.json index 75c5fa9..ae34ff0 100644 --- a/.specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/verification.json +++ b/.specsync/changes/CHG-0024-encrypted-file-secret-store-via-swift-crypto-issue-35/verification.json @@ -1,9 +1,9 @@ { - "timestamp": 1784439636, - "commit": "80068c8bbfcff3fc429485601c82a5e5b6894f66", + "timestamp": 1784441572, + "commit": "1186171781863b10457af7306a1ef4abe9fb59d5", "contract_digest": "827ed26145641b8b0d33b4e64e3404a86c75fa2f53393feaeead8250d493964d", - "workspace_digest": "f187d3481fcb19b1cb722e10459b3f6f15cc8240ca1bb0f9953201bdd00b2b7f", - "acceptance_input_digest": "33ae13c6d9d779995339e9b09a72e77f66e805760020544294b3489381fc9606", + "workspace_digest": "6687504506a657b5aa02ea267f5760956e4dd1f54169fa120f08fe3912336ac7", + "acceptance_input_digest": "4321ebcac73fdd2f8fa56fc3b04796ac3f88b645274cf96b5bdf0ee1715c6c29", "acceptance_manifest": { "schema_version": 1, "entries": [ @@ -111,8 +111,8 @@ "path": "Tests/apsTests/APSTests.swift", "kind": "file", "mode": 33188, - "payload_digest": "085adc68eb6c47c04b30d22df1bf8d62265593ce46beaccc16ff5dbbb00b7821", - "entry_digest": "330affb934181c0cde7bc76bdaf26c050583b473c9227ff0546d6049edb283f3", + "payload_digest": "c1db6d2e46fe9e66646508ce0391c62d216c00a687853f0dc7b2ce70e615beb0", + "entry_digest": "3da5367d8112cd896ba420e03463a17acd9ba705c457c8a087bc9585e9ad9459", "owners": [ "@exact:test" ] diff --git a/Tests/apsTests/APSTests.swift b/Tests/apsTests/APSTests.swift index 56bddcc..9848d12 100644 --- a/Tests/apsTests/APSTests.swift +++ b/Tests/apsTests/APSTests.swift @@ -193,6 +193,7 @@ final class APSTests: XCTestCase { XCTAssertFalse(FileManager.default.fileExists(atPath: fileURL.path)) } + #if !os(Windows) @MainActor func testSecretKeyFilePermissionsAre0600() async throws { let path = FileManager.defaultFileStatePath @@ -206,6 +207,7 @@ final class APSTests: XCTestCase { let attributes = try FileManager.default.attributesOfItem(atPath: keyURL.path) XCTAssertEqual(attributes[.posixPermissions] as? Int, 0o600) } + #endif @MainActor func testSecretStoreCorruptEnvelopeThrowsDecodingFailed() async throws { From 7c9c20124971b597787d2e4c5bf94f3ede1babc0 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 19 Jul 2026 12:24:21 +0000 Subject: [PATCH 14/17] Fix: retire stale Keychain secret requirements after encrypted store Supersede REQ-aps-cli-015 and REQ-state-store-013, and update testing notes to match the issue #35 EncryptedFile backend. Co-authored-by: Leif --- specs/aps-cli/requirements.md | 29 ++++++----------------------- specs/aps-cli/testing.md | 2 +- specs/state-store/requirements.md | 5 +---- specs/state-store/testing.md | 2 +- 4 files changed, 9 insertions(+), 29 deletions(-) diff --git a/specs/aps-cli/requirements.md b/specs/aps-cli/requirements.md index 8dd7af9..f5f0ec3 100644 --- a/specs/aps-cli/requirements.md +++ b/specs/aps-cli/requirements.md @@ -17,14 +17,12 @@ Acceptance Criteria ### REQ-aps-cli-002 -`set` SHALL reject values that cannot parse to the key's type and exit non-zero via `APSError.invalidValue`. All domain errors SHALL follow the error contract: human line on stderr, taxonomy exit code, and a JSON envelope when `--json` / `--jsonl` or `APS_ERROR_JSON=1`. +`set` SHALL reject values that cannot parse to the key's type and exit non-zero via `APSError.invalidValue`. Acceptance Criteria - Non-integer `counter` values fail with an invalid-value message. - Non-boolean `flag` values fail with an invalid-value message. - `APSError.description` names the key and expected type. -- Exit codes: 64 usage, 65 corrupt or undecodable persisted state, 69 unavailable, 70 internal, 73 write did not persist. -- The envelope shape is `{"error":{"code","message","hint"}}` with stable snake_case codes (`invalid_value`, `encoding_failed`, `decoding_failed`, `persistence_failed`, `keychain_unavailable`, `corrupt_state`); stdout stays empty on error. ### REQ-aps-cli-003 @@ -70,15 +68,12 @@ Acceptance Criteria ### REQ-aps-cli-012 -`watch` SHALL support `--count`, `--timeout`, and `--jsonl`, and SHALL handle SIGINT/SIGTERM with observable termination semantics. +`watch` SHALL support `--count`, `--timeout`, and `--jsonl`. Acceptance Criteria -- `--count` stops after that many printed values including the initial value (exit 0). -- `--timeout` stops after the given seconds (exit 124). -- SIGINT/SIGTERM stop the loop cleanly (exit 130 / 143). -- The stop reason appears as a terminal `{"type":"end","reason":...}` event in `--jsonl` mode or a stderr line in human mode. -- The `--jsonl` stream never contains non-JSON lines. -- An unbounded watch prints a one-time stderr hint suggesting `--count` / `--timeout`. +- `--count` stops after that many printed values including the initial value. +- `--timeout` stops after the given seconds. +- `--jsonl` emits one JSON object per line. ### REQ-aps-cli-013 @@ -100,9 +95,7 @@ Acceptance Criteria ### REQ-aps-cli-015 -Superseded by REQ-aps-cli-020 (encrypted-file secret store; issue #35). - - +Superseded by REQ-aps-cli-020 (encrypted-file secret store; issue #35). The Keychain-backed SecureState demo was removed because ad-hoc CLI signatures cannot earn durable Keychain trust. ### REQ-aps-cli-016 @@ -133,16 +126,6 @@ Acceptance Criteria - `specs/aps-cli/testing.md` and README document the Windows test + smoke path. -### REQ-aps-cli-019 - -Human output SHALL be TTY-aware under the git porcelain rule: pretty for interactive humans, byte-stable plain text when piped. JSON SHALL be pretty on TTY and compact when piped. - -Acceptance Criteria -- Piped `keys` output is the TSV form with no ANSI escapes; TTY gets an aligned table with bold headers and semantic color honoring NO_COLOR. -- `dump` / `--json` payloads are single-line compact JSON off-TTY and pretty on TTY. -- `watch --json` behaves as `--jsonl`; `keys --quiet` prints key names only. -- Shell completion scripts (bash/zsh/fish) are documented in README. - ### REQ-aps-cli-020 The `secret` key SHALL be backed by an encrypted-file secret store under the state root (ephemeral X25519 + HKDF + ChaCha20-Poly1305 via swift-crypto), with zero interactive prompts in key-file mode and passphrase mode via `APS_SECRET_PASSPHRASE`. diff --git a/specs/aps-cli/testing.md b/specs/aps-cli/testing.md index 5bc6433..bb070cb 100644 --- a/specs/aps-cli/testing.md +++ b/specs/aps-cli/testing.md @@ -6,7 +6,7 @@ - Smoke: `Scripts/smoke.sh` (Unix) and `Scripts/smoke.ps1` (Windows / PowerShell) for flag/note persistence, reset, and `aps stats` - Windows CI: `swift test` + `Scripts/smoke.ps1` on `windows-latest` (`windows-smoke.yml`) -- SecureState `secret` round-trip / Keychain delete (Darwin) or keychainUnavailable (else). +- Encrypted-file `secret` round-trip / wrong-passphrase `secretUnlockFailed` / corrupt envelope `decodingFailed`. - Slice `profileName` writes land in parent `profile` FileState. - Torn FileState files surface `corruptState` (exit 65) on get/watch; missing files stay nil/initial. diff --git a/specs/state-store/requirements.md b/specs/state-store/requirements.md index 017b2c1..488ef36 100644 --- a/specs/state-store/requirements.md +++ b/specs/state-store/requirements.md @@ -72,10 +72,7 @@ Acceptance Criteria ### REQ-state-store-013 -Superseded by REQ-aps-cli-020 (encrypted-file secret store via `SecretStore`; issue #35). - - - +Superseded by REQ-aps-cli-020 (encrypted-file secret store via `SecretStore`; issue #35). Keychain-backed SecureState paths and `keychainUnavailable` were removed from this CLI. ### REQ-state-store-014 diff --git a/specs/state-store/testing.md b/specs/state-store/testing.md index 01e4896..383cb12 100644 --- a/specs/state-store/testing.md +++ b/specs/state-store/testing.md @@ -7,7 +7,7 @@ - DemoStats ObservedDependency records mutations and Combine publishes on change - watchStatsBlocking detects dependency mutation -- SecureState `secret` round-trip / Keychain delete (Darwin) or keychainUnavailable (else). +- Encrypted-file `secret` round-trip / wrong-passphrase `secretUnlockFailed` / corrupt envelope `decodingFailed`. - Slice `profileName` writes land in parent `profile` FileState. - `read*FromDiskIfPresent` returns nil when absent and throws `corruptState` when torn. From 58a072da13a69cb6e00a8b0cfb456a62cd3ac706 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 19 Jul 2026 13:42:44 +0000 Subject: [PATCH 15/17] Fix: restore stacked smoke coverage after secret rebase Co-authored-by: Leif --- Scripts/smoke.sh | 93 +++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 81 insertions(+), 12 deletions(-) diff --git a/Scripts/smoke.sh b/Scripts/smoke.sh index 0ed9faa..212459a 100755 --- a/Scripts/smoke.sh +++ b/Scripts/smoke.sh @@ -19,30 +19,30 @@ test "$("$bin" --version)" = "0.2.0" "$bin" keys | grep -q counter "$bin" keys | grep -q profile "$bin" keys | grep -q secret -"$bin" keys --json | grep -q '"key" : "profile"' -"$bin" keys --json | grep -q '"key" : "secret"' +"$bin" keys --json | grep -q '"key":"profile"' +"$bin" keys --json | grep -q '"key":"secret"' # `set` prints the value; State is process-local so don't expect get in a new process. test "$("$bin" set counter 11)" = "11" test "$("$bin" set message "smoke")" = "smoke" -"$bin" set counter 11 --json | grep -q '"value" : 11' +"$bin" set counter 11 --json | grep -q '"value":11' # StoredState / FileState must survive process boundaries. "$bin" set flag true >/dev/null test "$("$bin" get flag)" = "true" -"$bin" get flag --json | grep -q '"value" : true' +"$bin" get flag --json | grep -q '"value":true' "$bin" set note "smoke-note" >/dev/null test "$("$bin" get note)" = "smoke-note" "$bin" set profile '{"name":"smoke","version":2}' >/dev/null -"$bin" get profile --json | grep -q '"name" : "smoke"' -"$bin" get profile --json | grep -q '"version" : 2' +"$bin" get profile --json | grep -q '"name":"smoke"' +"$bin" get profile --json | grep -q '"version":2' # Encrypted-file secret store: key-file mode round-trip, perms, reset. "$bin" set secret "smoke-secret" >/dev/null test "$("$bin" get secret)" = "smoke-secret" -"$bin" get secret --json | grep -q '"storage" : "EncryptedFile"' +"$bin" get secret --json | grep -q '"storage":"EncryptedFile"' if [[ "$(uname -s)" == "Darwin" ]]; then test "$(stat -f "%Lp" "$APS_HOME/secret.key")" = "600" else @@ -67,9 +67,9 @@ OTHER="$(mktemp -d "${TMPDIR:-/tmp}/aps-smoke-other.XXXXXX")" test "$("$bin" get note --state-dir "$OTHER")" = "other-root" test "$("$bin" get note)" = "smoke-note" -"$bin" dump | grep -q '"key" : "flag"' -"$bin" dump --json | grep -q '"key" : "profile"' -"$bin" dump --json | grep -q '"key" : "secret"' +"$bin" dump | grep -q '"key":"flag"' +"$bin" dump --json | grep -q '"key":"profile"' +"$bin" dump --json | grep -q '"key":"secret"' "$bin" reset flag >/dev/null test "$("$bin" get flag)" = "false" @@ -77,7 +77,7 @@ test "$("$bin" get flag)" = "false" "$bin" reset note >/dev/null test -z "$("$bin" get note)" -"$bin" reset profile --json | grep -q '"reset" : "key"' +"$bin" reset profile --json | grep -q '"reset":"key"' "$bin" reset --all >/dev/null test "$("$bin" get flag)" = "false" @@ -87,7 +87,7 @@ test -z "$("$bin" get note)" "$bin" watch counter --count 1 --timeout 2 >/dev/null # ObservedDependency stats command (process-local; fresh process starts at 0). -"$bin" stats --json | grep -q '"mutationCount" : 0' +"$bin" stats --json | grep -q '"mutationCount":0' "$bin" stats --watch --count 1 --timeout 2 >/dev/null # Invalid values should fail clearly. @@ -96,4 +96,73 @@ if "$bin" set counter nope >/dev/null 2>&1; then exit 1 fi +# Error contract: taxonomy exit codes, stdout purity, JSON envelope. +out="$("$bin" set counter nope 2>/dev/null)" && { echo "expected invalid counter to fail" >&2; exit 1; } +test $? -eq 64 || { echo "expected exit 64 for invalid value" >&2; exit 1; } +test -z "$out" || { echo "stdout must stay empty on error" >&2; exit 1; } +err="$("$bin" set counter nope --json 2>&1 >/dev/null || true)" +echo "$err" | grep -q '"code":"invalid_value"' + +# Corrupt persisted state exits 65 with a corrupt_state envelope. +echo 'garbage{{' > "$APS_HOME/note.json" +out="$("$bin" get note 2>/dev/null)" && { echo "expected corrupt note to fail" >&2; exit 1; } +test $? -eq 65 || { echo "expected exit 65 for corrupt state" >&2; exit 1; } +test -z "$out" || { echo "stdout must stay empty on corrupt state" >&2; exit 1; } +err="$("$bin" get note --json 2>&1 >/dev/null || true)" +echo "$err" | grep -q '"code":"corrupt_state"' +"$bin" reset note >/dev/null + +# Unwritable state root exits 73 on write. +BADROOT="$(mktemp "${TMPDIR:-/tmp}/aps-smoke-file.XXXXXX")" +out="$("$bin" set note x --state-dir "$BADROOT" 2>/dev/null)" && { echo "expected unwritable root to fail" >&2; exit 1; } +test $? -eq 73 || { echo "expected exit 73 for unwritable state root" >&2; exit 1; } +rm -f "$BADROOT" + +# APS_ERROR_JSON=1 opts into structured errors without --json. +err="$(APS_ERROR_JSON=1 "$bin" set flag maybe 2>&1 >/dev/null || true)" +echo "$err" | grep -q '"code":"invalid_value"' + +# TTY contract: piped output stays plain (TSV, no ANSI), --quiet prints names only. +"$bin" keys | grep -q $'KEY\tTYPE' +if "$bin" keys | grep -q $'\x1b'; then + echo "piped keys output must not contain ANSI escapes" >&2 + exit 1 +fi +test "$("$bin" keys --quiet | head -1)" = "counter" +test "$("$bin" keys --quiet | wc -l | tr -d ' ')" = "7" + +# JSON is compact when piped (gh rule); --json accepted as --jsonl alias on watch. +"$bin" dump | grep -q '"key":"flag"' +"$bin" watch counter --count 1 --timeout 2 --json >/dev/null + +# Watch termination semantics: exit codes and stream markers. +"$bin" watch counter --count 1 --jsonl >/dev/null +"$bin" watch counter --count 1 --jsonl | grep -q '"reason":"count"' +out="$("$bin" watch counter --timeout 1 --jsonl >/dev/null 2>&1)" && { echo "expected timeout exit" >&2; exit 1; } +test $? -eq 124 || { echo "expected exit 124 for timeout" >&2; exit 1; } +out="$("$bin" watch counter --timeout 1 --jsonl 2>/dev/null || true)" +echo "$out" | grep -q '"reason":"timeout"' +"$bin" watch counter --jsonl > "$APS_HOME/watch.out" 2>/dev/null & +WPID=$! +sleep 1 +kill -INT $WPID +# Bounded wait: the signal must stop the watch within a few seconds. +RC=0 +for _ in $(seq 1 20); do + kill -0 $WPID 2>/dev/null || break + sleep 0.5 +done +if kill -0 $WPID 2>/dev/null; then + echo "watch did not stop on signal" >&2 + kill -KILL $WPID 2>/dev/null || true + exit 1 +fi +wait $WPID || RC=$? +test "${RC:-0}" -eq 130 || { echo "expected exit 130 for SIGINT, got ${RC:-0}" >&2; exit 1; } +grep -q '"reason":"sigint"' "$APS_HOME/watch.out" +if grep -vq '^{' "$APS_HOME/watch.out"; then + echo "jsonl stream must contain only JSON lines" >&2 + exit 1 +fi + echo "smoke ok" From 0aebf4fff6492ed90bd0404588169db10af5c403 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 19 Jul 2026 13:43:13 +0000 Subject: [PATCH 16/17] Fix: enable EncryptedFile secret checks in smoke.ps1 Co-authored-by: Leif --- Scripts/smoke.ps1 | 23 ++++++++--------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/Scripts/smoke.ps1 b/Scripts/smoke.ps1 index 05b52b5..70708fa 100644 --- a/Scripts/smoke.ps1 +++ b/Scripts/smoke.ps1 @@ -108,21 +108,14 @@ $profileJson = Invoke-ApsOk get profile --json Assert-Match $profileJson '"name":"smoke"' 'profile name' Assert-Match $profileJson '"version":2' 'profile version' -# SecureState / Keychain smoke temporarily disabled (Keychain prompts / hangs). -# Re-enable with: APS_SMOKE_SECURESTATE=1 -if ($env:APS_SMOKE_SECURESTATE -eq '1') { - if ($IsMacOS) { - $null = Invoke-ApsOk set secret smoke-secret - Assert-Equal 'smoke-secret' (Invoke-ApsOk get secret) 'get secret' - Assert-Match (Invoke-ApsOk get secret --json) '"storage":"SecureState"' 'secret storage' - $null = Invoke-ApsOk reset secret - $after = Invoke-ApsOk get secret - if (-not [string]::IsNullOrEmpty($after)) { - throw "expected empty secret after reset, got '$after'" - } - } else { - Invoke-ApsExpectFail set secret smoke-secret - } +# Encrypted-file secret store (issue #35): key-file mode round-trip + reset. +$null = Invoke-ApsOk set secret smoke-secret +Assert-Equal 'smoke-secret' (Invoke-ApsOk get secret) 'get secret' +Assert-Match (Invoke-ApsOk get secret --json) '"storage":"EncryptedFile"' 'secret storage' +$null = Invoke-ApsOk reset secret +$after = Invoke-ApsOk get secret +if (-not [string]::IsNullOrEmpty($after)) { + throw "expected empty secret after reset, got '$after'" } # --state-dir overrides APS_HOME From 210365be0ba61828a81e1ed6362afc8193de9db5 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 19 Jul 2026 13:43:47 +0000 Subject: [PATCH 17/17] Fix: unset APS_SECRET_PASSPHRASE before key-file smoke checks Co-authored-by: Leif --- Scripts/smoke.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Scripts/smoke.sh b/Scripts/smoke.sh index 212459a..692ea05 100755 --- a/Scripts/smoke.sh +++ b/Scripts/smoke.sh @@ -40,6 +40,8 @@ test "$("$bin" get note)" = "smoke-note" "$bin" get profile --json | grep -q '"version":2' # Encrypted-file secret store: key-file mode round-trip, perms, reset. +# Clear passphrase env so key-file mode is exercised (agents may inherit it). +unset APS_SECRET_PASSPHRASE APS_SECRET_USE_PASSPHRASE || true "$bin" set secret "smoke-secret" >/dev/null test "$("$bin" get secret)" = "smoke-secret" "$bin" get secret --json | grep -q '"storage":"EncryptedFile"'