Skip to content

Community 1.1: daemon-owned core and Apple application finalization - #26

Open
bob-codedaptive wants to merge 13 commits into
develop/1.1.xfrom
codex/community-1.1-apple-finalization-r1
Open

Community 1.1: daemon-owned core and Apple application finalization#26
bob-codedaptive wants to merge 13 commits into
develop/1.1.xfrom
codex/community-1.1-apple-finalization-r1

Conversation

@bob-codedaptive

Copy link
Copy Markdown
Member

Outcome

Publishes the complete Community 1.1 daemon-owned core and macOS Community application projection.

  • daemon owns estate lifecycle, capture, reviews, Obsidian sync, transfer jobs, and default-off LAN serving
  • macOS Community UI binds the final daemon contracts for setup, capture, reviews, Obsidian, transfer, and LAN controls
  • installer packages the signed launchd daemon provider with fail-closed App Group provisioning validation
  • release workflow consumes the daemon provisioning profile from GitHub Actions secrets
  • public-edition guardrails prevent private Fulcrum/ProductDock/CloudKit surfaces from entering the CE artifact

Measured gates

  • Swift core: 843 tests, twice consecutively
  • Rust: 476 tests
  • real daemon subprocess golden harness: 60 fixtures plus negative cases
  • CommunityBoundaryTests: 204 tests
  • final public-head macOS app: Developer ID signed, Apple notarization accepted, stapled, distribution verifier passed
  • local arm64 package: daemon profile and signed-entitlement readback passed

Release gate

No release tag is created by this PR. Final publication remains gated on the physical launchd acceptance ledger.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 940cab3489

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

// estate at glk-estate.sqlite in the layout directory. In production this
// is a fully encrypted estate; in contract tests it is plaintext (both
// paths share this same code — the key provider handles the difference).
let glkEstateURL = layoutURL.appendingPathComponent("glk-estate.sqlite")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Use the canonical estate for GLK-backed features

Capture, lifecycle, and review all open layoutURL/estate.sqlite, but this composition creates a separate glk-estate.sqlite and gives its handle to the Obsidian and transfer coordinators. Consequently, captures made through the Community UI are absent from exports and Obsidian sync, while imported records are written to a database that capture, review, and the daemon descriptor never expose. Open the canonical estate through GeniusLocusKit instead of creating a second estate.

Useful? React with 👍 / 👎.

guard !isOperationInFlight else { return }
isOperationInFlight = true
defer { isOperationInFlight = false }
exportDestinationOutcome = await port.selectExportDestination()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Invalidate plans after changing transfer selections

When an export plan already exists, selecting or cancelling a different destination only replaces exportDestinationOutcome; exportPlan remains permitted, so the Execute button stays enabled and submits the old token, whose daemon-side record still names the previous destination. This can export data to a location the user just replaced or cancelled. Clear the plan and downstream job state whenever the destination or scope changes; selectImportSource() has the analogous stale-plan path.

Useful? React with 👍 / 👎.

Comment on lines +200 to +202
groups = entitlements.get("com.apple.security.application-groups", [])
if "group.com.codedaptive.mootx01" not in groups:
raise SystemExit("profile does not grant group.com.codedaptive.mootx01")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Accept the prefixed app-group value in the profile guard

Both release jobs pass the daemon provisioning profile through this check, but it requires the bare group.com.codedaptive.mootx01 value while the generated daemon entitlement and this commit's Community artifact verifier require the team-prefixed value (<TEAM>.group.com.codedaptive.mootx01). A correctly provisioned release profile therefore reaches this branch and aborts both macOS package builds before signing. Validate the same prefixed spelling used by the signed entitlement, optionally allowing an explicitly supported alternate form.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d8bbfed563

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +368 to +370
let urlString = String(data: bookmark, encoding: .utf8),
let vaultURL = URL(string: urlString),
vaultURL.isFileURL

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Resolve the security-scoped bookmark emitted by the app

The shipped macOS UI sends opaque data from URL.bookmarkData(options: .withSecurityScope) in DaemonCommunityFeaturePorts.swift, not a UTF-8 URL string, so this conversion normally fails and every real Obsidian vault selection is denied before the directory check. CommunityTransferCoordinator.resolveBookmarkToURL repeats the same assumption, making import-source and export-destination selection fail as well; resolve the bookmark with the bookmark APIs and retain security-scoped access for the operation instead.

Useful? React with 👍 / 👎.

Comment on lines +786 to +791
// Job is running — it will cancel at the next write-window boundary.
// Return cancelled{beforeCommit} optimistically; the Task will update to
// the correct stage (duringCommit or afterCommit) when it terminates.
// The sidecar update happens asynchronously in the Task.
log.info("jobCancel: running job cancel requested: \(jobID, privacy: .public)")
return JobCancelOutcome.cancelled(stage: .beforeCommit).toJSONValue()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Do not report a running cancellation as before-commit

When cancellation arrives after a job has entered running, this returns cancelled(beforeCommit) even though both job runners perform the entire importSeed or export call before checking the flag again. The UI stores this response verbatim, so it can tell the user that no work was committed while the import or export is still writing and may later finish with afterCommit; wait for the runner's actual stage or otherwise avoid returning the before-commit guarantee.

Useful? React with 👍 / 👎.

Comment on lines +270 to +273
self.plans = [:]
self.cancelFlags = [:]
// Load persisted jobs from sidecar; start fresh if missing or corrupt.
self.jobs = Self.loadJobs(at: layoutURL)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reconcile nonterminal jobs after a daemon restart

If the daemon restarts while a persisted job is queued or running, initialization reloads that state but recreates neither its background task nor its cancellation flag. Subsequent status calls therefore report the nonterminal state forever, and an exact execute retry only returns the existing job ID without restarting work; reconcile loaded nonterminal records by safely resuming them or transitioning them to an explicit interrupted/failed terminal state.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bcd1945249

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +379 to +380
servingState = .active(fd: fd, port: actualPort, credential: credential, serverTask: serverTask)
return LANStartOutcome.started(endpoint: endpoint, authentication: .valid).toJSONValue()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Provide clients with the minted LAN credential

When LAN serving starts, every /records request requires this bearer token, but the production response exposes only the endpoint and authentication state; the only token accessor in the repository is the test-only testToken(). Consequently, even an authority-approved start creates a server that no remote client can authenticate to. Add a secure production pairing or credential-delivery path before reporting the service as usable.

Useful? React with 👍 / 👎.

Comment on lines +367 to +374
if let recovered = allDrawers.first(where: { $0.addedBy == addedByMarker }) {
// The previous attempt committed to the estate. Rebuild the ledger
// entry so future retries take the fast path, then return the receipt.
let contentHash = captureContentHash(
content: arguments.content,
subject: arguments.subject
)
let recoveredEntry = LedgerEntry(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject conflicting retries during ledger recovery

If the daemon crashes after estate.capture() commits but before the ledger write, a retry with the same request ID but different content, destination, or policy enters this recovery branch. It finds the old drawer by addedBy but constructs the recovered ledger entry from the new arguments without comparing them to the committed drawer, then returns the old record as successfully applied; this defeats the documented request-conflict guarantee and permanently associates the old record with the new payload hash and policy. Validate the recovered drawer against the retry or reconstruct the original receipt without accepting conflicting arguments.

Useful? React with 👍 / 👎.

Comment on lines +378 to +381
let receiptID = metadata?.receiptID ?? UUID().uuidString.lowercased()
return LifecycleMCPResponse.wrap(LifecycleStateBuilder.ready(
estate: estateSummary,
receiptID: receiptID

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep estate receipt IDs stable when metadata is absent

For an existing estate whose new estate-metadata.json sidecar is absent or unreadable, every inspect() call generates a fresh receipt UUID here rather than synthesizing and persisting stable metadata. This is also reached after create() silently fails to write the sidecar, so reconnects present different receipt identities for the same estate and violate the model's stable-receipt contract. Derive or persist one stable receipt ID, and avoid reporting creation success when that persistence fails.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7e62237049

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +166 to +168
locusEstate = try await Estate.open(
storage: storage,
owner: OwnerCredentials(ownerIdentifier: ownerIdentifier),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Do not create the estate during provider activation

On every fresh production install, DaemonProvider.activate() calls this host before the community dispatcher becomes reachable, and Estate.open creates the missing estate.sqlite. Consequently, the lifecycle coordinator can never return needsCreation; its create(name:) path is always refused because the file already exists, so the first-run UI cannot create or name the estate. Activation needs a non-creating readiness state, or the automatically created estate must still be surfaced as requiring lifecycle setup.

Useful? React with 👍 / 👎.

Comment on lines +587 to +590
// secret tier: never exports (excluded above by .sensitivityAtMost(.secret))
// private tier: excluded unless scope is .believedIncludingPrivate
let privateExcluded = exportScope.includesPrivateTier ? 0 :
allInScope.filter { $0.sensitivity == .restricted }.count

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Count secret drawers as export exclusions

When an existing or imported secret-tier drawer is also marked exportable, .sensitivityAtMost(.secret) includes it—the ceiling includes every tier through secret—but this partition counts only .restricted drawers as excluded. The plan therefore overstates estimatedTransferCount and may permit execution when every candidate is secret, while VaultBridge.export later excludes those records and writes an empty export. Partition secret-tier records here using the same rules as DrawerMapping.export.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ad6045523c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

let key = try KeychainKeyStore(
service: "com.codedaptive.mootx01",
estateURL: url,
accessGroup: "com.codedaptive.mootx01.shared"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Pass the expanded keychain access group

In a signed production daemon, the entitlement and ProviderEligibility.expandedKeychainGroup use <TEAMID>.com.codedaptive.mootx01.shared, but the estate opener passes the bare suffix directly as kSecAttrAccessGroup. Because KeychainKeyStore does not expand this value, the initial estate.openEstate() during provider activation cannot read or create the database key and fails with a Keychain entitlement error, preventing the resident daemon from starting. Thread the already-validated expanded group into both estate key-provider closures.

Useful? React with 👍 / 👎.

Comment on lines +306 to +307
let currentFingerprint = CommunityReviewEngine.estateFingerprint(
activeDrawers: drawers.filter { $0.tombstonedAt == nil }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Exclude system drawers from the action fingerprint

When captureChoices() has initialized an empty estate, it creates a system:capture_choices sentinel. generateSession() excludes that sentinel when producing sourceEstateState, but this staleness check includes every non-tombstoned drawer, so the fingerprints can never match and every first-time review action returns staleSession even when no user data changed. Apply the same non-system predicate used by session generation and duplicate resolution; the re-apply fingerprint above has the same mismatch.

Useful? React with 👍 / 👎.

Comment on lines +461 to +463
// ideally merges the older drawer's content into the newer before archiving, but
// Estate.mutate() is internal to LocusKit. Both choices produce the same archive
// effect here; content merge is deferred pending a public mutation API.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Implement merge before archiving duplicates

When the user selects the advertised “Merge content into the newer record” choice, choiceIndex never affects execution and the coordinator immediately archives every older drawer exactly as it does for the keep-newer choice. For same-subject duplicates whose contents differ, this reports the merge as applied while making the older content disappear without copying it into the retained drawer. Until an actual mutation API is wired, refuse or omit this choice rather than performing the destructive archive-only operation.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant