Fleet accumulated fixes (2026-07-17) - CI verification#21
Conversation
Flip the default managed-service URL from the Railway hostname to the custom domain team.engraphis.com, and mark the old Railway host as the retired URL that client-side key/relay resolution migrates automatically (signatures preserved). Docs (.env.example, README) and the migration tests are updated to match.
…ility gaps Licensing: - Register endpoint only seat-caps Team, not every plan. Pro is the individual multi-device tier and must not be device-capped, matching sync_relay._authorize; capping it locked a paying Pro customer's second device out of ALL paid features (register issues the lease that gates them). Adds test_pro_register_is_not_device_capped. - Trial-email rate limit now keys off the rightmost (trusted-proxy) X-Forwarded-For entry, so a client can't mint fresh buckets by prepending a spoofed value. Billing webhooks: - Add order.refunded / subscription.revoked / subscription.canceled handling -> revoke every key for the subscription (new license_registry.revoke_by_subscription); previously refunded buyers kept working keys until natural expiry. - Webhook reservation is now tri-state (claim_webhook): an in-flight attempt returns a retryable 503 instead of a 2xx duplicate, so a crash mid-fulfillment no longer cancels Polar's retries and loses the purchase. - subscription.updated is ordered by the event's modified_at; an out-of-order (older) delivery can no longer regress a newer seat count. - A mid-cycle seat-change reissue is bounded to current_period_end (+grace) instead of a fresh full 35/395-day window from now, so it can't extend entitlement past the paid period. - Enforce POLAR_ORGANIZATION_ID when set; support an explicit ENGRAPHIS_POLAR_PRODUCT_MAP tier override (keeps the never-stiff-a-payer default).
…lete/merge - /api/intent/remember and /api/intent/link now require _paid(team), matching /api/remember. They write to the same cloud store, so without the gate a free / license-lapsed instance could host unlimited agent writes (paywall bypass). - min_role now requires admin for /api/workspaces/delete and /api/workspaces/merge. These irreversibly destroy/combine a whole shared workspace, so a plain member must not be able to run them (they were member-gated while less destructive ops like export/import were admin).
- Wrap the v2 Store's single cross-thread sqlite connection in _SerializedConnection, which holds a reentrant lock for the duration of each write transaction (pinned via in_transaction, released on commit/rollback). A bare check_same_thread=False connection let threadpool handlers interleave transactions, so one thread's commit/rollback could land on another's uncommitted writes. Read-only statements lock per-call; a failed statement never auto-rolls-back (that would break the probe-optional-table-and-continue pattern) and lock acquisition times out rather than deadlocking. - v1 background decay (apply_decay_to_all) now anchors on a new last_decay column advanced each pass, so an interval is decayed exactly once. The old code reapplied a fixed days-since-access factor to the already-decayed value every ~60s loop tick and collapsed all stability to the floor.
… removes access now Per product rule: a paid-then-canceled subscription keeps its plan until the paid period ends (its period-bounded key simply expires), while a REFUND pulls the license immediately. Drop subscription.canceled/cancelled from the immediate-revoke set; keep order.refunded and subscription.revoked. Adds tests for both the keep-until-period-end and immediate-removal paths.
…ss-loop recall
- list_documents(offset=..) with no limit no longer emits invalid SQL
(OFFSET without LIMIT -> 500); uses LIMIT -1.
- GET /memory/documents/{id} without ?namespace looks the doc up across all
namespaces via find_document() instead of a nonexistent '_global' (always 404).
- POST /memory/prune honors an explicit minRetention=0.0 instead of coercing to 0.05.
- POST /memory/conversations returns 400 for a user message missing 'content'
instead of crashing 500.
- POST /memory/interactions now reinforces memories mentioning the entity
(reweight.boost_entity_memories wires the previously-dead apply_interaction_boost).
- recall_master(namespace=None) recalls across ALL namespaces, so the consciousness
loop's thought synthesis and /memory/memories/context stop silently no-opping.
…tor open redirect - /api/auth/setup creates exactly one admin atomically (create_user require_empty guard inside its BEGIN IMMEDIATE), closing the multi-admin TOCTOU. - Session cookie Secure flag set when the EXTERNAL connection is HTTPS, including behind a TLS-terminating proxy (X-Forwarded-Proto); request.url.scheme alone dropped Secure for every proxied deployment. - The :8710->:8700 redirector no longer reflects the untrusted Host/X-Forwarded-Host into its 301 target (open redirect); the destination origin is fixed by config.
…neration - current_license()/invalidate_cache() now guard the process-wide license cache (_cached/_cache_error/_cache_recheck_at) with a reentrant lock, so a concurrent invalidate between the "is not None" check and the return can no longer make current_license() return None. The network cloud gate still runs OUTSIDE the lock (idempotent on a cache miss), so checks are not serialized. - machine_id() generates the device id under a lock and persists it with an atomic O_CREAT|O_EXCL create, so two concurrent threads/processes on a first run can't each register a different id and burn an extra Team seat.
…re validity; guard autosync policy - Store.get_or_create_workspace now authorizes the RETRIEVE path too, so a workspace outside ENGRAPHIS_WORKSPACES that already exists (predating the allow-list or arriving via sync) can't be handed back, bypassing isolation. - Sync no longer clamps world-time validity (valid_from/valid_to) to now+skew. Those are bi-temporal WORLD time (a fact can be valid until a future date) and don't feed the LWW version key, so clamping truncated real future validity and the earliest-wins merge spread it to every device. System timestamps (last_access/ingested_at/expired_at) stay clamped (anti-poison). - autosync._record no longer resets the policy to the disabled default on a transient read failure of an existing policy file (it distinguishes a fresh install from an unreadable existing file), so a read hiccup after a sync pass can't silently turn auto-sync off.
…compose defaults - CORS default loopback origins follow ENGRAPHIS_PORT instead of hardcoding 8700, so a non-default port doesn't lock the dashboard's own origin out. - AuthStore.login runs PBKDF2 OUTSIDE the serialized lock (only the DB/throttle steps hold it). The async auth middleware resolves sessions under the same lock, so holding it across the ~100ms hash stalled the whole event loop — a burst of logins was an unauthenticated DoS. - docker-compose: the v1 `api` profile now uses its own DB file (engraphis_v1.db) instead of sharing the v2 dashboard's engraphis.db (incompatible schemas corrupt both); env_file .env is marked required:false so a fresh clone (no .env) can still `docker compose up`.
…graphis_remember/record_event) regression guard vs fleet-wide 'workspace Field required'
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9430b40ed2
ℹ️ 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".
| with _cache_lock: | ||
| if lic is not None: | ||
| _cached, _cache_error = lic, "" | ||
| _cache_recheck_at = _license_recheck_at(lic) |
There was a problem hiding this comment.
Prevent stale lease checks from re-enabling revoked keys
When two current_license(refresh=True) calls overlap (or invalidate_cache() runs while one is in flight), a newer authoritative denial can store the free tier first, then an older allowed _cloud_gate response reaches this block and overwrites it with the paid license. That re-enables a revoked/refunded key until the next 900-second recheck, despite the intended immediate fail-closed behavior; retain a validation generation or otherwise reject results superseded by an invalidation/denial.
Useful? React with 👍 / 👎.
| # Phase 3 (locked): record the outcome and mint the session. | ||
| with self._lock: | ||
| if not ok or row is None or row["disabled"]: | ||
| fails = self._failures.pop(email, []) | ||
| fails.append(time.time()) | ||
| self._failures[email] = fails | ||
| self._prune_throttle_maps(time.time()) | ||
| self.record_event("login.failed", actor_email=email, ip=ip, | ||
| detail=("account_disabled" if row and row["disabled"] | ||
| else "bad_credentials")) | ||
| raise AuthError("invalid email or password") | ||
| self._failures.pop(email, None) | ||
| token = self.create_session(row["id"]) | ||
| user = self.get_user(row["id"]) |
There was a problem hiding this comment.
Recheck login state after unlocked password verification
With concurrent login attempts, every request that reaches phase 1 before the first PBKDF2 finishes observes the same pre-lockout state; this block records failures but never rechecks _locked_until, so a burst can make far more than LOCKOUT_FAILS password guesses before the throttle takes effect. The same stale row also permits a login that began just before a password reset or disable to mint a new session afterward. Re-fetch/revalidate the user and lockout state after reacquiring the lock before creating the session.
Useful? React with 👍 / 👎.
|
test-1784315776 |
Council Gate Verdict — APPROVERepository: Coding-Dev-Tools/engraphis Per-model scores
Generated by the Model Council PR gate ( |
|
Council Merge Gate -- REWORK This PR was submitted to the model-council merge gate after the council-review approval. The merge gate returned REWORK (not a clean APPROVE), so it was left open and not merged. Please address the council's concerns and re-run the review gate before merge. (This is an automated notice; no human was overridden.) |
) Follow-up hardening for the accumulated fleet commits: - test_licensing_concurrency.py (new): thread-race regressions for 7b62157 — machine_id() yields ONE id under 16 racing first-run threads (no extra Team seat), and current_license() never returns None under concurrent invalidate_cache(). - test_auth_concurrency.py: prove login runs PBKDF2 OUTSIDE AuthStore._lock so a concurrent lock-taking op is not blocked by an in-flight hash (e8d4ac9). - test_intent_paywall.py (new): pin the _paid(team) 402 gate on /api/intent/remember and /api/intent/link, plus the licensed 200 path (5a31389 gated both routes but tested neither). - v2_team._cookie_secure: document the non-proxied-HTTP X-Forwarded-Proto caveat. Behavior intentionally unchanged — gating XFP behind the empty-default ENGRAPHIS_FORWARDED_ALLOW_IPS would drop Secure on proxied deploys; XFP can only ADD Secure, never downgrade. - docker-compose.yml: note the optional-.env object form needs Compose v2.24+ and give the older-version fallback.
Accumulated fixes committed locally by the Cowork fleet (hourly-dev-engineer), 2026-07-17.
Pushed for clean-CI verification before merging to main. 11 commits, 34 files, +1463/-184.
Council Gate Verdict — REWORK
Risk level: high | Agreement: ? | Council session:
council-491f8b95-1ddc-471b-bf62-6fccb69db574Per-model scores (rubric: correctness / safety / style / tests / complexity)
Engraphis reference
Council verdict persisted as
mem_01KXRQJDBE3NMCC2Y885MAQSN3(workspacehermes-council).