add astra draft
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
# Acceptance and verification
|
||||
|
||||
## Executed in the implementation environment
|
||||
|
||||
- Python API tests: 21 passing, including strict validation, inheritance, optimistic revisions, authorization boundaries, cookie-origin checks, session rotation, persistence, code expiry, reward caps, idempotency and concurrent submission.
|
||||
- Swift PolicyCore tests: 10 passing under Swift 6.2.1 on Linux, including day/DST boundaries, bedtime, zero allowances, monotonic usage observations, cap truncation and exhaustive reachable-bonus threshold coverage.
|
||||
- JavaScript syntax check; Swift source syntax parsing; YAML/plist/source-path validation.
|
||||
- Swift networking source type-check against Linux Foundation with a platform import and local error stub. This is not an Apple SDK build.
|
||||
- Chromium DOM + real backend smoke via an in-process ASGI bridge: login, two children, shared policy, individual override, pairing, graded credit, refreshed dashboard and logout. It does not test deployed browser cookies, TLS or a physical iPhone. Parent session/origin behavior is separately covered by API tests.
|
||||
|
||||
Not executed here: XcodeGen project generation, Xcode compile/link/archive/sign/install, Docker build/run, Screen Time authorization, shield appearance/action on hardware, or real DeviceActivity scheduling/counting. Do not mark these as passed based on pure Swift tests.
|
||||
|
||||
## First build-server pass
|
||||
|
||||
- [ ] `make bootstrap && make test` succeeds on the build server.
|
||||
- [ ] `make ios-project` generates the four-target project.
|
||||
- [ ] Xcode builds with the selected SDK; check all target bundle IDs and App Group entitlements.
|
||||
- [ ] Archive/sign/install using owner-supplied profiles. Verify all extensions are embedded.
|
||||
- [ ] Docker starts with a generated secret and persistent volume; restart retains profiles and awards.
|
||||
- [ ] HTTPS and exact `APC_ORIGIN` work from parent browser and child phone; no plaintext fallback.
|
||||
|
||||
## Physical iPhone matrix
|
||||
|
||||
Use a dedicated test entertainment app. Start with a small base quota and the minimum five-minute reward, not essential communication apps. Record target OS version and observed callback times; the implementation intentionally does not fake sub-minute accuracy.
|
||||
|
||||
- [ ] Child Family Sharing account: approve `.child` authorization, then cancel and retry authorization.
|
||||
- [ ] Pair once; code reuse/expired code fails. Reopening the app does not expose unrestricted selection editing.
|
||||
- [ ] Parent-selected app is actually shielded outside allowed hours. Close on the shield exits correctly.
|
||||
- [ ] Base cumulative usage reaches its threshold and shields the app with Family Quests backgrounded.
|
||||
- [ ] Five correct answers record one credit and unlock further **usage** time. Wrong answers earn none.
|
||||
- [ ] Putting the phone down does not consume a wall-clock reward; reaching the next usage threshold re-shields.
|
||||
- [ ] Repeated Sync, foreground transitions, retrying submission, duplicate/late callbacks, and relaunches do not refund usage.
|
||||
- [ ] Earning the last partial reward hits the exact cap. Another challenge cannot earn more that day.
|
||||
- [ ] Earn a bonus before base time is exhausted; cumulative accounting remains correct.
|
||||
- [ ] Bedtime shields despite unused/earned allowance. Bonus expires at the family-day boundary.
|
||||
- [ ] Keep the main app closed overnight: the next allowed window restores base only, not yesterday's bonus. Repeat over two offline days.
|
||||
- [ ] Force quit, reboot, lock/unlock and first unlock after reboot: no unexpected clearing or permanent accidental unlock.
|
||||
- [ ] Change policy mid-day (raise/lower base, reward size, cap and allowed hours); prior usage is not incorrectly refunded and obsolete callbacks do not win.
|
||||
- [ ] Change selected apps with a fresh parent code. Verify removed/new apps and cumulative historical usage; selection changes are a known accounting risk.
|
||||
- [ ] Fail monitoring registration / corrupt or make shared state unavailable in a test build: error is visible, no unconditional `clearAllSettings` recovery.
|
||||
- [ ] Offline backend, connection timeout, low-power mode and stale snapshots: cached limits remain; no unverified credit is minted.
|
||||
- [ ] Change device timezone and clock; verify configured family timezone and check the five-minute foreground skew guard. Offline clock tampering remains a known limitation.
|
||||
- [ ] Two children inherit a shared edit; only the child with an override retains its different value. Pending/error/last-reported status is truthful.
|
||||
- [ ] Parent code expires and loses validity on leaving the setup session. A child cannot point an already-paired app at a replacement server to authorize selection changes.
|
||||
- [ ] Revoke a device: API access stops, cached restrictions remain. Test same-server re-pairing with a new parent-issued code.
|
||||
- [ ] A stricter native Apple App Limit still wins. Do not interpret that as a successful override of Apple's native limit.
|
||||
|
||||
## Stop conditions
|
||||
|
||||
Treat early/late/missing threshold callbacks, double-counted usage, unexpected unlocks, deadlocks during monitor replacement, storage races, inaccessible essential apps or a guardian recovery failure as blockers to real use. Capture diagnostic logs with test profiles rather than real family data. Do not hide these failures behind a green `applied` label.
|
||||
@@ -0,0 +1,40 @@
|
||||
# Apple build and entitlement setup
|
||||
|
||||
## Build targets
|
||||
|
||||
| Target | Bundle ID suffix | Extension point |
|
||||
|---|---|---|
|
||||
| FamilyQuests | `.app` | App |
|
||||
| ActivityMonitor | `.app.monitor` | `com.apple.deviceactivity.monitor-extension` |
|
||||
| ShieldConfiguration | `.app.shield` | `com.apple.ManagedSettingsUI.shield-configuration-service` |
|
||||
| ShieldAction | `.app.shieldaction` | `com.apple.ManagedSettings.shield-action-service` |
|
||||
|
||||
The prefix is `APC_BUNDLE_PREFIX`. All targets use `APC_APP_GROUP`; the app and monitor actually exchange state through it. Defaults are in `ios/Config/Base.xcconfig`. Copy `Local.xcconfig.example` to the gitignored `Local.xcconfig` and supply the team and identifiers you control.
|
||||
|
||||
Enable the Family Controls capability and shared App Group for the app and applicable extensions, and use provisioning profiles containing the corresponding entitlements. The checked-in entitlement file does not grant Apple's permission by itself. Request Family Controls distribution authorization for the App IDs you intend to distribute and regenerate the matching profiles. Development provisioning and distribution approval are separate steps.
|
||||
|
||||
XcodeGen supplies the Info.plist extension identifiers/principal classes and embeds the extensions. Generated `.xcodeproj` and plists are gitignored. `make ios-project` is required after cloning or changing the specification. No signing certificates, profiles, private keys, Apple credentials or CI configuration are committed.
|
||||
|
||||
The app requests FamilyControls authorization for a **child**. Test with the intended Family Sharing child account and a guardian available to approve. There is no automatic fallback to `.individual`. An adult's test phone cannot stand in for testing the guardian/child authorization and anti-removal behavior.
|
||||
|
||||
## Network and local development
|
||||
|
||||
Use the parent dashboard's real HTTPS origin on the phone, with a trusted certificate. Configure the same origin on the server; a mismatching browser Origin causes parent mutations to fail. The iPhone's `localhost` refers to that iPhone, not the build server. Debug loopback HTTP is for simulator/local harness work only. A local-network usage description is included; inspect the phone's Local Network permission when testing a private LAN server.
|
||||
|
||||
Do not expose an unsigned debug API on the LAN or put the parent administration token in the iPhone app. Screen Time changes are applied on the child's device, not by the Mac browser calling Apple's settings remotely.
|
||||
|
||||
## Remaining distribution work
|
||||
|
||||
Supply app icons, final display metadata, privacy declarations/manifests appropriate to the actual shipped functionality, provisioning, signing/export options and deployment automation. No App Store/TestFlight approval or successful archive is claimed. Keep native Screen Time safety restrictions in place while validating the prototype.
|
||||
|
||||
## Primary references
|
||||
|
||||
- [Meet the Screen Time API](https://developer.apple.com/videos/play/wwdc2021/10123/)
|
||||
- [What's new in Screen Time API](https://developer.apple.com/videos/play/wwdc2022/110336/)
|
||||
- [Family Controls authorization](https://developer.apple.com/documentation/familycontrols/authorizationcenter/requestauthorization(for:))
|
||||
- [DeviceActivity event includesPastActivity](https://developer.apple.com/documentation/deviceactivity/deviceactivityevent/includespastactivity)
|
||||
- [DeviceActivity startMonitoring](https://developer.apple.com/documentation/deviceactivity/deviceactivitycenter/startmonitoring(_:during:events:))
|
||||
- [Requesting the Family Controls entitlement](https://developer.apple.com/documentation/familycontrols/requesting-the-family-controls-entitlement)
|
||||
- [XcodeGen project specification](https://github.com/yonaskolb/XcodeGen/blob/master/Docs/ProjectSpec.md)
|
||||
|
||||
These describe platform mechanisms, not evidence that this particular prototype works correctly on your hardware. Record the OS, Xcode, provisioning and observed behavior in the acceptance checklist.
|
||||
@@ -0,0 +1,81 @@
|
||||
# Architecture and invariants
|
||||
|
||||
## Components
|
||||
|
||||
Parent browser -> same-origin FastAPI service -> SQLite policy, pairing and reward records.
|
||||
Child SwiftUI app -> authenticated HTTPS API -> validated snapshot -> local Apple enforcement.
|
||||
DeviceActivity extension -> shared App Group state -> named ManagedSettings store.
|
||||
|
||||
The monitor extension does **not** contact the backend and does not receive its credentials. App/category tokens stay on the phone. Backend truth is limited to policies, submitted challenge results, awarded credit and device-reported status; it is not an independent observation of actual screen use.
|
||||
|
||||
## Policy inheritance
|
||||
|
||||
`effective_policy = family_policy + child.overrides` (shallow merge of a validated, flat schema).
|
||||
|
||||
Empty overrides inherit everything. Clearing an override restores inheritance. Updating shared policy validates every child's resulting policy in the same transaction. A global monotonically increasing revision and `expected_revision` prevent silent lost updates. v0 invalidates outstanding challenges on any policy revision, including a change affecting another child; finer per-child revisions can be added later.
|
||||
|
||||
The timezone is family-wide. Days and bonus caps are computed by the server using that IANA timezone, not a device-supplied date. The native monitor uses the same timezone. Changes to the timezone after rewards exist are blocked until a proper ledger migration is implemented.
|
||||
|
||||
Default limits are configurable examples, not scientific or clinical recommendations.
|
||||
|
||||
## Reward integrity
|
||||
|
||||
Each challenge contains generated question IDs and server-retained correct answers. The client receives prompts only. Submission must answer every question exactly once with strict integer values; the server checks those values rather than trusting a `completed` flag or client-supplied minutes.
|
||||
|
||||
An unfinished, unexpired challenge is reused instead of issuing parallel attempts. Attempts expire after ten minutes and at a family-day boundary or policy revision change. A wrong submission is finalized without credit; the child can start another challenge. Attempts are capped at 100 per device/day as a basic resource bound.
|
||||
|
||||
Submission uses a SQLite `BEGIN IMMEDIATE` transaction. A unique `challenge_id` on rewards plus a persisted challenge result makes retries and concurrent submissions idempotent. The awarded amount is `min(reward_size, remaining_daily_cap)`. Existing grants are summed per child/day and clamped to the current cap in snapshots. There is no client endpoint for arbitrary credit issuance.
|
||||
|
||||
This verifies correctness, not who did the work. A calculator, another person, or a modified client can solve arithmetic too; v0 does not claim learning-outcome attestation.
|
||||
|
||||
## Local usage enforcement
|
||||
|
||||
The app validates the snapshot, preserves usage observations for the current day, and installs a repeating allowed-hours monitor with cumulative usage events. The threshold ladder includes:
|
||||
|
||||
1. Base allowance and each subsequent reward increment through the cap, including a partial final reward.
|
||||
2. The current earned offset and subsequent increments, so a mid-day reward-size change remains representable.
|
||||
|
||||
Both the ordinary and shifted ladders are registered. On a new day, the ordinary ladder remains valid even without a server connection. Bonus entitlement is only honored on its snapshot day. Observations roll over by policy timezone, not by a foreground-only timer.
|
||||
|
||||
A callback records the maximum reached threshold. Duplicate or out-of-order lower thresholds do not refund usage. The decision is shielded if outside allowed hours, allowance reached, or monitoring failed. A new ordinary reward just changes entitlement and reevaluates the shield; it does not restart the monitor. Policy/selection changes or lost registration install a new monitor with `includesPastActivity: true`. This is intended to preserve cumulative accounting; exact Apple behavior must be checked on the target OS.
|
||||
|
||||
Names contain an installation generation so obsolete monitor callbacks cannot overwrite the current plan. A file lock plus atomic JSON replacement serializes app/extension updates. Failed monitor registration leaves the selected apps shielded. Unreadable shared state does not clear existing ManagedSettings. This is defensive behavior, not a guarantee against an OS failing to deliver callbacks. Midnight, policy replacement, late callbacks and restarts are explicit hardware acceptance gates.
|
||||
|
||||
Only individual apps are supported initially. Whole categories/websites are deferred to avoid expanding enforcement and usage-accounting assumptions before the first physical tests. The app does not promise to override another Screen Time controller or a stricter native limit.
|
||||
|
||||
## Authentication and synchronization
|
||||
|
||||
The bootstrap parent token lives in server configuration and is never sent to the child. Parent browser sessions are random, HttpOnly, SameSite=Strict cookies; their stored hash is keyed by the admin secret, so rotating it invalidates sessions. Cookie-authenticated mutations require the configured Origin. Parent API callers may use the bootstrap bearer directly.
|
||||
|
||||
Pairing/configuration codes carry 144 bits of randomness, expire in ten minutes, are stored hashed, and are consumed atomically. Device bearer tokens are child/device-scoped and hashed at rest on the server. The app stores its token and anchored server origin in Keychain, not UserDefaults or the extension's container. Parent-selection access lasts one short foreground setup session; Apple guardian authorization is requested separately with `.child`, never silently downgraded to individual/self-control authorization.
|
||||
|
||||
Remote updates apply at the next foreground/manual sync. `status` is a device acknowledgement with a revision, monitoring state, shield flag, bounded detail, and last threshold observation. Parent status deliberately says **last reported**, not independently verified or live. There is no push delivery or retry worker in v0.
|
||||
|
||||
Revoking pairing disables API access immediately but does not remotely remove local restrictions. The retired device retains its last cached rules. To replace a revoked pairing on the same phone, issue a new pairing code and use Parent setup -> Replace revoked pairing. The phone keeps the old server origin anchored. Server migration/decommissioning needs a designed guardian recovery flow before production use.
|
||||
|
||||
## API surface
|
||||
|
||||
| Role | Endpoint | Purpose |
|
||||
|---|---|---|
|
||||
| Parent | `POST/DELETE /v1/session` | Browser sign-in/sign-out |
|
||||
| Parent | `GET /v1/parent/family` | Policy, children, overrides, pairing/status summaries |
|
||||
| Parent | `PUT /v1/parent/policy` | Revision-checked shared policy replacement |
|
||||
| Parent | `POST /v1/parent/children` | Create a child display-name profile |
|
||||
| Parent | `PUT /v1/parent/children/{id}/overrides` | Replace sparse child overrides |
|
||||
| Parent | `POST /v1/parent/children/{id}/pairing-code` | Issue a one-use enrollment code |
|
||||
| Parent | `POST /v1/parent/devices/{id}/configuration-code` | Permit one selection-editing session |
|
||||
| Parent | `DELETE /v1/parent/devices/{id}` | Revoke API access |
|
||||
| Enrollment | `POST /v1/pair` | Consume code; return token once |
|
||||
| Device | `POST /v1/device/configuration-unlock` | Consume a device-specific parent code |
|
||||
| Device | `GET /v1/device/snapshot` | Validated effective policy and today's bonus |
|
||||
| Device | `POST /v1/device/status` | Report local applied/error status |
|
||||
| Device | `POST /v1/device/challenges` | Start/reuse generated challenge |
|
||||
| Device | `POST /v1/device/challenges/{id}/submit` | Grade and issue bounded, idempotent reward |
|
||||
|
||||
Snapshot v1 fields: `schema_version`, `device_id`, `child_id`, `child_name`, `policy_revision`, `policy`, `day`, `earned_minutes`, `server_time` (Unix seconds). JSON uses snake_case, with shared Swift encoder/decoder conventions. Policy fields are listed in `backend/src/apc/models.py`; `packages/PolicyCore` implements their native validation and semantics.
|
||||
|
||||
Submission result fields: `challenge_id`, `passed`, `correct_count`, `required_count`, `awarded_minutes`, nullable `reward_id`, and `day`. A credit being recorded is distinct from a subsequent successful snapshot application.
|
||||
|
||||
## Planned extensions
|
||||
|
||||
The narrow first slice is deliberately not a general quest engine. Add manual chores/approval and an explicit grant ledger API after iPhone enforcement is validated. Each new reward source must preserve idempotency, cap handling and the exact threshold ladder; arbitrary reward denominations need extra accounting work. Then add background synchronization, richer schedules and safe recovery. Do not introduce per-device copies of the family policy as a shortcut.
|
||||
@@ -0,0 +1,33 @@
|
||||
# Security and operations (development MVP)
|
||||
|
||||
This is not security-audited or ready for unattended child-safety enforcement. Start with test profiles and nonessential apps. Keep the native Apple safety baseline and a guardian recovery path.
|
||||
|
||||
## Deployment
|
||||
|
||||
The server accepts one household's bootstrap administration token, generated randomly with at least 32 characters. The browser exchanges it for a 12-hour HttpOnly, SameSite=Strict session cookie. HTTPS origins produce Secure cookies. Changing `APC_ADMIN_TOKEN` invalidates old browser sessions on the next request. Never commit `.env`, device tokens, pairing/configuration codes, backups, signing credentials or real child profiles.
|
||||
|
||||
Use a trusted HTTPS reverse proxy. Bind the upstream privately, keep the configured browser Origin exact, and add connection/rate limits and a **32 KiB request-body cap at the proxy, including chunked requests**. The app checks declared Content-Length; it is not a streaming-body firewall. Add TLS/HSTS and proxy access controls appropriate to the deployment. No CORS exceptions or wildcard origins are configured. Do not directly expose the raw development service to the public internet.
|
||||
|
||||
The supplied Compose service is non-root, read-only except for its named data volume and temporary directory, drops capabilities and binds to loopback. Its Docker build/runtime still needs verification on the target host. Python dependencies are exact versions from the implementation environment; perform dependency vulnerability scanning and update/retest them before deployment. There is no automatic update mechanism.
|
||||
|
||||
## Credential and client boundaries
|
||||
|
||||
Parent administration credentials never belong on a child's phone. Device credentials authorize only the paired child's API operations. Codes and device tokens are hashed in SQLite; parent session hashes are keyed by the admin secret. Pairing and configuration codes use high-entropy random values rather than guessable short PINs.
|
||||
|
||||
The native app stores credentials in a device-only Keychain item. App Group state holds policy, app-selection tokens, quota observations and monitor configuration, not API credentials. Redirects are refused by the native HTTP client and plain LAN HTTP is not enabled. An existing pairing anchors the server origin; re-pairing needs a new code from that same server.
|
||||
|
||||
The threat model assumes an unmodified signed app and normal device sandboxing. A jailbroken phone, compromised parent/server, stolen bearer token, forged native status report, calculator-assisted solution, or OS scheduling failure is not solved by this MVP. Parent status is explicitly last-reported, not independent proof of enforcement.
|
||||
|
||||
## Data and retention
|
||||
|
||||
The server stores child display names, overrides, device labels, hashed credentials, policy revisions, generated arithmetic questions/solutions, correctness counts, awarded credit and limited device status. It does not upload application tokens, app names, URLs, browsing history, per-app usage reports, health data, or Apple account credentials. There are no analytics/advertising SDKs.
|
||||
|
||||
SQLite is not encrypted by the application. Protect the host, filesystem permissions and backups, and use encrypted disks/backups where appropriate. No automatic retention/deletion UI is implemented. A production version needs explicit family export/deletion and retention policies before taking real users' data. Do not infer regulatory compliance from this document.
|
||||
|
||||
## Persistence and recovery
|
||||
|
||||
Schema version 1 is initialized in `store.py`. Future schema changes require a real versioned migration; do not silently edit an existing schema in place. Refuse to run older code against a newer database. Back up with SQLite's backup API or while the service is stopped; copying only the live `.sqlite3` file can miss WAL data.
|
||||
|
||||
Revoking API access does not remove restrictions from an offline phone. Same-server re-pairing is supported with a new parent-issued code after revocation. Complete server loss/migration, child removal, and factory-reset/Keychain lifecycle still need a designed recovery flow. Before testing, verify that the guardian can revoke Family Controls authorization/remove the experimental app through Apple's supported controls. Never select essential communication or the task app itself.
|
||||
|
||||
Cached policy continues locally when the backend is unavailable. Monitor registration failures deliberately keep selected apps shielded, but missing callbacks can still cause incorrect behavior. The application is not a tamper-proof replacement for MDM or all native Screen Time settings.
|
||||
Reference in New Issue
Block a user