Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
.DS_Store
.public-safety.local
node_modules/
agent-harness/node_modules/
agent-harness/artifacts/
Expand Down
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ Current status:
- direct and service-backed broker failures now expose stable exit codes for invalid requests, unavailable capacity, repair-needed aliases, override-required flows, and internal failures
- `host init --bootstrap-config` now provisions real simulator devices, including dual iPhone UI aliases (`ui-1`, `ui-2`), a second dedicated `build-fast` alias for overlapping build-test demand, and records their actual IDs and runtime versions in host config
- `erase-on-acquire` leases now run behind a dedicated reset lock and roll back cleanly if reset fails before a lease is handed out
- lease acquisition prefers a matching pin, then compatible warm capacity, then
shutdown capacity; the most recently released alias wins inside each tier and
the broker boots it before returning success
- optional Automatic shutdown is configured only through broker commands, has
no source-defined duration, preserves every registered alias as standby, and
excludes pins, leases, manual aliases, and unhealthy devices
- a macOS operator app now exists under `app/` with Overview, Simulators, Projects, and Events screens plus broker-backed actions for pinning, release, and lifecycle control
- the macOS app overview, empty-state, simulator-detail, destructive-confirmation, and override-required remediation flows now have captured operator-facing evidence; the app also supports alternate broker roots with `--state-root`, `--host-config`, optional `--cli-path`, and direct deep-link review targeting
- the broker now publishes a canonical `app-snapshot.json` read model under the broker state root for the app and smoke tooling
Expand All @@ -28,6 +34,8 @@ Current status:
- repo capacity can now be diagnosed with `simbroker capacity check`; missing
broker-managed capacity can be previewed with non-mutating `capacity
reconcile` and applied only with exact human confirmation of the current plan
- idle policy can be inspected and reconciled with `simbroker idle`; one-time
cleanup uses a count-only preview followed by exact human confirmation
- broker-aware sample consumer repo artifacts now cover manual human, interactive agent, unattended agent, and CI patterns under `examples/harness-adoption/`
- a guide-aligned `broker-harness-adoption` skill lives under `.agents/skills/broker-harness-adoption/`
- the repo is ready for continued implementation on top of the active specs
Expand Down Expand Up @@ -129,6 +137,7 @@ npm run package:distribution

```bash
npm test
npm run verify:public-surface
npm run test:install-smoke
npm run test:app
npm run test:client
Expand All @@ -138,6 +147,12 @@ node client/bin/simbroker.mjs app snapshot
node client/bin/simbroker.mjs capacity check --repo-root "$PWD" --purpose agent-ui-session --json
node client/bin/simbroker.mjs capacity reconcile --repo-root "$PWD" --purpose agent-ui-session --json
node client/bin/simbroker.mjs capacity reconcile --repo-root "$PWD" --purpose agent-ui-session --apply --confirm <plan-id> --actor-type human --actor-id <operator-id> --json
node client/bin/simbroker.mjs idle status --json
node client/bin/simbroker.mjs idle enable --grace-seconds <60-86400> --actor-type human --actor-id <operator-id> --json
node client/bin/simbroker.mjs idle disable --actor-type human --actor-id <operator-id> --json
node client/bin/simbroker.mjs idle reconcile --json
node client/bin/simbroker.mjs idle cleanup --json
node client/bin/simbroker.mjs idle cleanup --apply --confirm <plan-id> --actor-type human --actor-id <operator-id> --json
node client/bin/simbroker.mjs pin create --repo-root "$PWD" --purpose manual-testing --alias manual-1
node client/bin/simbroker.mjs lease release --lease-file /tmp/simbroker-lease.json
node client/bin/simbroker.mjs simulators boot --alias ui-1
Expand All @@ -147,6 +162,13 @@ npm run agent:context -- --paths spec/README.md --session-dir "$HOME/.codex/agen
npm run agent:verify -- --profile spec-only --paths spec/README.md --session-dir "$HOME/.codex/agent-harness/simulator-broker-app/bootstrap"
```

Automatic shutdown is unconfigured on a fresh install. Choose a valid duration
explicitly in the app or CLI; do not create or edit broker state files by hand.

For an additional machine-local public-safety check, create an ignored
`.public-safety.local` with one private name, alias, or path per line. The
scanner reports matching rule numbers without printing the private values.

## Security

Please report security issues privately through GitHub Security Advisories when
Expand All @@ -165,4 +187,5 @@ Contributor setup, verification, and PR expectations are documented in
- [spec/architecture.md](spec/architecture.md)
- [spec/implementation-plan.md](spec/implementation-plan.md)
- [spec/harness-integration.md](spec/harness-integration.md)
- [spec/tasks/public-safe-on-demand-simulator-lifecycle.md](spec/tasks/public-safe-on-demand-simulator-lifecycle.md)
- [references/README.md](references/README.md)
2 changes: 1 addition & 1 deletion agent-harness/tests/runtime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ describe("runtime argument parsing", () => {

expect(doctorJson.baseProfiles).toEqual(["implementation"]);
expect(doctorJson.obligationProfiles).toEqual([]);
});
}, 15_000);

it("reports macOS build tools for app implementation doctor output", () => {
const output = runHarnessCommand([
Expand Down
9 changes: 9 additions & 0 deletions app/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ Current implementation:
- XCTest coverage under `app/Tests/`
- runtime data source is the broker-owned `app-snapshot.json` artifact under the broker state root
- broker mutations are sent through the local `brokerd` Unix socket so the app shares the same authority as the CLI
- Overview includes Automatic shutdown status, explicit duration entry,
Apply/Disable actions, and count-confirmed cleanup over broker transport
- local-debug packaging stays available through `scripts/package_local.sh`
- Release distribution packaging now lives in `scripts/package_distribution.sh` and writes an explicit readiness summary instead of implying shipping readiness

Expand Down Expand Up @@ -44,6 +46,8 @@ npm run package:distribution
If you already have the repo checkout on the target machine, prefer `npm run install:local` plus `source "$HOME/Library/Application Support/SimulatorBroker/install/env.sh"` over `npm run package:local`.

`bash scripts/test_app.sh` writes stable result bundles under `artifacts/app-tests/` by default and prints the exact `xcodebuild` command it executes, so focused reruns can be collected into a task session without reconstructing the command by hand.
Each run also gives the XCTest host a fresh temporary broker state root and
host-config path instead of touching the default local broker installation.

The Codex app `Run` action is expected to point at `./script/build_and_run.sh`.

Expand All @@ -57,8 +61,13 @@ When the local service is running, the app can:
- create and clear pins
- release active leases
- request `boot`, `shutdown`, `erase`, and `repair`
- configure or disable Automatic shutdown with an explicitly entered valid duration
- preview a count and confirm one-time cleanup of currently idle automated simulators
- surface broker override-required errors and ask the human for confirmation details

Automatic shutdown is unconfigured initially, so the duration field is blank.
The app never writes host configuration, policy, or state files directly.

The current local install flow copies the built app bundle to `~/Applications/Simulator Broker.app` by default.

For smoke tests or alternate broker fixtures, override the state root at launch time:
Expand Down
162 changes: 162 additions & 0 deletions app/Sources/AutomaticShutdownSection.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import SwiftUI

struct AutomaticShutdownSection: View {
@Bindable var store: BrokerDashboardStore
let idle: BrokerIdleSummary

@State private var graceSecondsText = ""

var body: some View {
GroupBox("Automatic shutdown") {
VStack(alignment: .leading, spacing: 16) {
summaryGrid

Divider()

controls

Text(inputGuidance)
.font(.caption)
.foregroundStyle(graceSecondsText.isEmpty || graceSeconds != nil ? Color.secondary : Color.red)
}
.frame(maxWidth: .infinity, alignment: .leading)
}
.onAppear(perform: synchronizeInput)
.onChange(of: idle.graceSeconds) { _, _ in
synchronizeInput()
}
.confirmationDialog(
Text("Clean idle simulators now?"),
isPresented: cleanupConfirmationPresented,
presenting: store.pendingIdleCleanupRequest
) { request in
Button(cleanupButtonTitle(count: request.eligibleCount), role: .destructive) {
store.confirmIdleCleanup()
}
} message: { request in
Text(cleanupConfirmationMessage(count: request.eligibleCount))
}
}

private var summaryGrid: some View {
LazyVGrid(
columns: [
GridItem(.flexible(minimum: 220), spacing: 18),
GridItem(.flexible(minimum: 220), spacing: 18),
],
alignment: .leading,
spacing: 12
) {
LabeledContent("Policy") {
StatusPill(
color: idle.configured ? .green : .secondary,
title: idle.configured ? "Configured" : "Not configured"
)
}
LabeledContent("Grace duration", value: configuredDurationText)
LabeledContent("Eligible now", value: "\(idle.eligibleCount)")
LabeledContent("Last result", value: lastResultText)
}
}

private var controls: some View {
HStack(alignment: .firstTextBaseline, spacing: 12) {
TextField("60–86400 seconds", text: $graceSecondsText)
.frame(width: 180)
.textFieldStyle(.roundedBorder)
.accessibilityLabel("Automatic shutdown grace duration in seconds")

Button("Apply", action: applyPolicy)
.buttonStyle(.borderedProminent)
.disabled(graceSeconds == nil || store.canSendCommands == false || store.isApplyingAction)

Button("Disable", action: disablePolicy)
.disabled(idle.configured == false || store.canSendCommands == false || store.isApplyingAction)

Spacer()

Button("Clean idle simulators now", role: .destructive) {
store.requestIdleCleanup()
}
.disabled(store.canSendCommands == false || store.isApplyingAction)
}
}

private var cleanupConfirmationPresented: Binding<Bool> {
Binding(
get: { store.pendingIdleCleanupRequest != nil },
set: { isPresented in
if isPresented == false {
store.pendingIdleCleanupRequest = nil
}
}
)
}

private var configuredDurationText: String {
guard let graceSeconds = idle.graceSeconds else {
return "Not configured"
}
return "\(graceSeconds) seconds"
}

private var graceSeconds: Int? {
guard let value = Int(graceSecondsText), (60 ... 86_400).contains(value) else {
return nil
}
return value
}

private var inputGuidance: String {
if graceSecondsText.isEmpty {
return "Enter a whole number of seconds from 60 through 86400."
}
if graceSeconds == nil {
return "The duration must be a whole number from 60 through 86400 seconds."
}
return "Unused automated simulators are shut down after this grace period."
}

private var lastResultText: String {
guard let result = idle.lastCleanupResult else {
return "No cleanup recorded"
}
let status = result.status.replacingOccurrences(of: "_", with: " ").capitalized
return "\(status) · \(result.shutdownCount) shut down · \(result.failureCount) need repair"
}

private func cleanupButtonTitle(count: Int) -> String {
count == 1 ? "Shut down 1 simulator" : "Shut down \(count) simulators"
}

private func cleanupConfirmationMessage(count: Int) -> String {
count == 1
? "1 currently idle simulator is eligible for shutdown."
: "\(count) currently idle simulators are eligible for shutdown."
}

private func synchronizeInput() {
graceSecondsText = idle.graceSeconds.map(String.init) ?? ""
}

private func applyPolicy() {
guard let graceSeconds else { return }
Task { @MainActor in
do {
try await store.applyIdlePolicy(graceSeconds: graceSeconds)
} catch {
store.lastErrorMessage = error.localizedDescription
}
}
}

private func disablePolicy() {
Task { @MainActor in
do {
try await store.disableIdlePolicy()
} catch {
store.lastErrorMessage = error.localizedDescription
}
}
}
}
7 changes: 7 additions & 0 deletions app/Sources/BrokerCommandSupport.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,13 @@ struct BrokerPendingLeaseReleaseRequest: Identifiable {
var id: String { lease.leaseId }
}

struct BrokerPendingIdleCleanupRequest: Identifiable {
let eligibleCount: Int
let planId: String

var id: String { planId }
}

private struct BrokerDashboardStoreFocusedKey: FocusedValueKey {
typealias Value = BrokerDashboardStore
}
Expand Down
Loading