add astra draft

This commit is contained in:
2026-09-17 17:59:34 +02:00
parent 88588dcfcc
commit 41458afc76
38 changed files with 2392 additions and 0 deletions
+81
View File
@@ -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.