chore: import relay VFS DOM semantic fuse and auto-skill discovery order from PekingSpades fork - #2
Merged
Conversation
Squashed import of 4 commits ahead of PekingSpades/synapse:main: ddaae82 Harden remote agent delivery and participant handling be3a433 refactor: centralize business enum usage 628d1a7 Add relay VFS browser/CUA mount support fdf34c9 Standardize relay VFS validation flow Method: pre-formatted both peking/main and peking/feat with zai-org's prettier+gofmt style, diffed the formatted trees, then applied with git apply --3way to suppress format-only conflicts. Resolved one semantic conflict in packages/web-next/app/dashboard/settings/model-group-detail.tsx (zai-org renamed strategyLabel to a local helper; kept the new shared model-group-shared imports from the fork). Source: https://github.com/PekingSpades/synapse/tree/feat/relay-vfs-dom-semantic-fuse
…fork Squashed import of 1 commit ahead of PekingSpades/synapse:main: 9ce134a fix: prioritize relay auto-loaded skills in discovery Method: pre-formatted both peking/main and peking/fix versions of the 5 skill files with zai-org's prettier+gofmt style, diffed the formatted trees scoped to those files, then applied with git apply --3way. No conflicts. Source: https://github.com/PekingSpades/synapse/tree/fix/relay-auto-skill-discovery-order
JoshZiel83
pushed a commit
to JoshZiel83/Synapse
that referenced
this pull request
Sep 19, 2026
…ed IP Review follow-ups zai-org#1 and zai-org#2: zai-org#1 — downloadToBuffer (the generic entry used by saveFromUrl, and thus by MCP image results, Zhipu image/parse URLs, files/ingest) had NO SSRF check and read the whole body unbounded (arrayBuffer). It now delegates to downloadToBufferWithLimit with a 50 MiB default cap, so every caller gets the SSRF host check + streaming size cap. zai-org#2 — assertPublicHost was pre-flight only: fetch re-resolved the hostname at connect time, leaving a DNS-rebinding window. Add ssrfSafeDispatcher() — an undici Agent whose connect-time lookup re-validates EVERY resolved address and refuses non-public ones, so the address that passes the check is the one the socket connects to. downloadToBufferWithLimit now fetches through it (plain Agent when allowPrivateHosts is set, e.g. tests/internal). undici declared as a direct api dep. Tests: +2 (generic entry enforces SSRF; hostname→loopback blocked at connect); storage 10 + ssrf 8 green.
JoshZiel83
pushed a commit
to JoshZiel83/Synapse
that referenced
this pull request
Sep 19, 2026
Review follow-up zai-org#2: the redirect branch did `await res.arrayBuffer()` to drain a 3xx body BEFORE the final response's Content-Length/streaming maxBytes checks — so a malicious server could attach a huge body to a 302 and blow past maxBytes. Switch to res.body?.cancel(), which frees the stream/connection without reading the bytes. Test: a 5 MiB redirect body with maxBytes=10 now succeeds on the small final body (proving the redirect body is never buffered/counted). storage 11 green.
JoshZiel83
pushed a commit
to JoshZiel83/Synapse
that referenced
this pull request
Sep 19, 2026
…ross side-effects Two more lock-correctness defects from review: zai-org#1 STRANDED WAKEUPS ON ACQUIRE-SKIP: session-thinking jobs are enqueued with no BullMQ attempts, so a job that finds the session lock held just returns "session locked" = done, no retry. Combined with the lock-loss restore path (restore wakeups -> pending -> requeue), the requeued job could hit the new owner's lock and skip, while that owner had already read 0 pending and gone idle -> wakeups stranded with no driver. Fix: on acquire-skip, if there are pending wakeups, re-enqueue a DELAYED "think" job (2s) so it re-claims once the current owner releases. Eventually-consistent: it keeps retrying until a worker holds the lock and drains the pending wakeups. zai-org#2 RENEW DIDN'T COVER SIDE-EFFECTS: the fenced lock-renew interval was cleared right after actorThink, but executeActorActions + message persistence + turn/wakeup status + runtime/audit/event emits all run AFTER that, until the outer finally releases. A side-effect phase longer than the 120s TTL could let another worker concurrently take over. Fix: hoist the interval and clear it in the OUTER finally instead, so the fenced renew covers the whole critical section (model + all side-effects) until release. api builds clean; full api suite 1296/1296 green.
JoshZiel83
pushed a commit
to JoshZiel83/Synapse
that referenced
this pull request
Sep 19, 2026
…ueue error Review follow-ups zai-org#1 and zai-org#2: zai-org#1 SIDE-EFFECTS WROTE AFTER LOCK LOSS: lockLost was checked once right after actorThink, but executeActorActions, message persistence, turn/wakeup status, audit/event, and the terminal idle/queued writes all ran afterward without re-checking — so a lock lost DURING the side-effect phase still let the old worker complete writes, racing the new owner. Add assertStillHoldLock() and call it before executeActorActions, before message persistence, before markTurnWakeupsProcessed/updateTurnStatus(completed), and before the terminal status write. Each throws SessionLockLostError → catch restores wakeups + requeues. (The renew now spans the whole critical section, so loss mid-side- effect is unlikely; these checks bound the residual double-write window.) zai-org#2 DISHONEST retryScheduled: the acquire-skip delayed re-enqueue swallowed a queue.add() failure yet still returned retryScheduled:true — monitoring would see "scheduled" while the wakeups were actually stranded. Let the add() throw (the job then fails visibly / is retried) since that re-enqueue is the only driver for the pending wakeups. api builds clean; full api suite 1298/1298 green. Renew-returns-false-on- takeover (the mechanism that sets lockLost) is covered by lock.test FENCING.
JoshZiel83
pushed a commit
to JoshZiel83/Synapse
that referenced
this pull request
Sep 19, 2026
…nger hangs Four review follow-ups on the lock-loss recovery: zai-org#1 — replay flag set too early: sideEffectsStarted flipped true before executeActorActions REGARDLESS of result.actions, so a pure-reasoning turn (no actions, no message — executeActorActions is a no-op, persistence kind:"none") was treated as replay-unsafe and its input wakeups were DROPPED on a lock loss. Rename to replayUnsafeStarted and set it true only when a non-idempotent write actually happens: result.actions.length > 0, or messagePersistence.kind !== "none". zai-org#2 — late independent wakeups stranded: the replay-unsafe branch skipped the pending-wakeup check, so a message that arrived mid-turn (never attached to this turn, and enqueueSessionWakeup won't enqueue a job while running) had no driver. Now BOTH branches re-check getPendingWakeupCount after the restore/drop and requeue if any remain. This never replays the current turn: in the unsafe case its own wakeups were dropped (not pending), so only genuinely-unprocessed input drives the re-run. zai-org#3 — concurrency test hung (exit 124): it materialized the ioredis singleton but never closed it. Add an `after()` hook calling shutdownRedisConnections(). Verified it now exits 0 even WITHOUT --test-force-exit. zai-org#4 — test didn't cover the real decision: extract decideLockLossRecovery() (pure: replayUnsafe → restore|drop, requeue iff pending remains) and route the handler through it, then drive the concurrency test through that same policy. Added the two regressions above as explicit cases (pure-reasoning replay-safe; replay-unsafe-but-late-pending requeues). api typecheck clean; full api suite 1370 pass / 0 fail / 25 skipped.
JoshZiel83
pushed a commit
to JoshZiel83/Synapse
that referenced
this pull request
Sep 19, 2026
…s, auth-origin plumbing High zai-org#1 (native email login/register session): the custom synapse_session cookie name broke @better-auth/expo's cookie-jar (it only persists cookies matching its prefix or *session_token). Revert to BA's DEFAULT cookie name (better-auth.session_token) — no business code reads the cookie by name anymore. getSessionBearerToken() now matches by endsWith('session_token') (handles the __Secure- prefix). session-provider also prefers signIn/signUp data.token, with the cookie-jar value as fallback. High zai-org#2 (native Feishu OAuth return): refreshSession() now falls back to the expo cookie-jar token (getSessionBearerToken) when SESSION_TOKEN_KEY is empty and persists it — so a deep-link return from the OAuth browser flow establishes the session. Medium zai-org#3 (web device polling): pollDeviceToken() unwraps the RFC 8628 HTTP-400 error body (authorization_pending/slow_down/access_denied/expired_token) from the thrown ApiError.details and returns { error } instead of throwing, so the QR panel's normal-wait and terminal branches work. Low zai-org#4 (mobile auth-origin plumbing): EXPO_PUBLIC_AUTH_ORIGIN wired through setup.sh + docker-compose mobile-web build args + Dockerfile.mobile-web (optional; defaults to API origin for same-origin deploys). Verified: server emits better-auth.session_token + set-auth-token + body token; simulated expo cookie-jar capture + bearer resolution succeed; api 1384/0/25; web typecheck 0; integration 20/20; mobile typecheck clean.
JoshZiel83
pushed a commit
to JoshZiel83/Synapse
that referenced
this pull request
Sep 19, 2026
…evice-poll error unwrap Medium zai-org#1: setup.sh only wrote AUTH_TRUSTED_ORIGINS in the new-.env template, so an existing .env (pre-feature or customized) never gained $APP_URL + synapse://, leaving the server's trustedOrigins empty -> @better-auth/expo can't return the session on a native Feishu OAuth deep-link callback. Added ensure_csv_env_contains (merge, not overwrite — preserves operator-added origins, no dup) and call it for AUTH_TRUSTED_ORIGINS in the upsert region. Verified the helper across missing/ merge/noop cases. Low zai-org#2: pollDeviceToken() only unwraps HTTP-400 responses whose body.error is a known RFC8628 device error (authorization_pending|slow_down|expired_token| access_denied|invalid_grant); 500/502/proxy/other errors now re-throw so the QR panel surfaces a real failure instead of masking it as 'expired'. web typecheck 0; setup.sh syntax OK.
JoshZiel83
pushed a commit
to JoshZiel83/Synapse
that referenced
this pull request
Sep 19, 2026
…ypes/markdown-it Finding zai-org#2 (prod-install reliability): @better-auth/expo's @better-auth/core peer was auto-resolving to the hoisted 1.6.14 while better-auth@1.6.13 nests an exact 1.6.13, so the Dockerfile prod path (npm ci --legacy-peer-deps --workspace=packages/api ...) could fail to find a consistent core. - root: add @better-auth/core@1.6.13 override + as a direct api dependency, so it lands at packages/api/node_modules/@better-auth/core in the prod layout. - mobile (separate lockfile): add @better-auth/core@1.6.13 dep + override so the hoisted copy is 1.6.13 (was 1.6.14). Plain 'npm install' (not --legacy-peer-deps) so nativewind's tailwindcss peer tree is preserved. Verified: clean Dockerfile-path npm ci resolves core@1.6.13 at packages/api/node_modules; betterAuth/bearer/genericOAuth/deviceAuthorization/ expo/getOAuth2Tokens all import; mobile expo->core@1.6.13 + subpaths resolve. Finding zai-org#3 (mobile typecheck): markdown-it@14 ships no types and @types/markdown-it was absent (pre-existing on dev, surfaced via chat-markdown-html.ts). Add @types/markdown-it@^14.1.2 (devDep). Mobile 'tsc --noEmit' now green. Verification after merging dev 3aea085 into the branch: db:rebuild + db:codegen:verify clean; api typecheck 0; guard:db OK (566 files); access/authz tests 191/191; full api suite 1475/1475; web build compiles; mobile typecheck 0.
JoshZiel83
pushed a commit
to JoshZiel83/Synapse
that referenced
this pull request
Sep 19, 2026
…e contracts Round-2 review: the shared schemas existed but weren't the runtime contract, and the devices write-path/cloud + service-shaping were still in the old world. zai-org#1/zai-org#2 devices WRITE-path → shared camelCase (master plan §5.1.1/§8.3): - shared/schemas/devices.ts gains app-facing input contracts (camelCase): CreateCloudDeviceInput, StartPairingInput, ClaimDaemonServiceInput, SetActiveDeviceCapabilitiesInput + CreateCloudDeviceResultView / ActiveDeviceCapabilitiesView. - controller parses these; device-sdk + web send camelCase bodies (device_type→deviceType, service_kind→serviceKind, remote_agent_machine_id→remoteAgentMachineId, device_capability_ids→deviceCapabilityIds, workspace_id→workspaceId, host_provider→hostProvider). - access-bindings POST now on the shared SetActiveDeviceCapabilitiesInput (the wire-shape test reframed to the app-facing contract); GET list key → deviceCapabilityIds. - cloud.ts result → camelCase; sandbox docker-backend + its test updated. - device-runtime startPairingSession consumes the shared camelCase StartPairingInput + DevicePairingTicketView. - removed the now-orphaned device-protocol input/result schemas (CreateCloudDeviceInput/Result, StartPairingInput, PairingTicket, ClaimDaemonInput, SetActiveDeviceCapabilitiesInput); kept the genuine wire ConsumePairing*/ScopedSubjectTargetWire. zai-org#3 model-groups + mcp-plugins controllers → sendData(ViewSchema, value): - 30 model-groups + 8 mcp-plugins sites now parse the shared View schemas at runtime (the schemas were "dead definitions"). Envelope → { data }. - updateModelGroup always returns ModelGroupDetailView (single schema for PUT). - web lib/api.ts unwraps { data } in-method, keeping each method's public shape stable so callers are unchanged. Endpoints without a View schema (auth-session/audit/install-plan) deliberately left as-is. zai-org#5 devices service no longer shapes DTOs: listDevices/getDevice/startPairing/ claimRemoteAgentDaemon return domain records (DeviceSummaryRecord/ DeviceDetailRecord/DevicePairingTicketRecord/DeviceServiceRecord); the controller calls the presenter. Added presentDevicePairingTicket. zai-org#4 mcp-plugins schemas: z.custom already gone (r1); documented the genuinely plugin-author-owned opaque JSONB (configSchema/configData/toolsManifest/ metadata) — the structured fields are already nested-schema'd. zai-org#7 device-sdk tests: added src/index.test.ts (0 → 8 tests) locking the camelCase send/parse boundary + the snake wire consume shape; build moved to tsconfig.build.json so tests stay out of dist. Verification: full monorepo build green; api 1640 pass/0 fail/2 skip; guards (layering/db/fk/soft-delete/datetime) clean; device-sdk 8/8, device-runtime 314/315(+1 skip), remote-agent-daemon 24/24; web + mobile typecheck clean.
JoshZiel83
pushed a commit
to JoshZiel83/Synapse
that referenced
this pull request
Sep 19, 2026
…ervice→presenter retreat Round-3 review: app-facing endpoints still on bare reply.send, /auth/me unmigrated, mcp-plugins auxiliary flows uncollected, and service still produced outward Views. zai-org#1 devices controller → sendData { data } (§8.3): list/detail/pairing/claim/cloud now sendData(ViewSchema, ...) → { data: ... }. device-sdk read methods unwrap res.data + parse the shared view; device-sdk tests updated to { data } fixtures. 204 endpoints (delete/detach) unchanged; consume/bootstrap stay bare wire. zai-org#2 access-bindings GET → sendData(ActiveDeviceCapabilitiesViewSchema, ...) → { data: { deviceCapabilityIds } }. POST stays 204 on the shared input contract. zai-org#3 mcp-plugins auxiliary endpoints collected to sendData with new shared schemas (PluginAuthSessionView/Envelope, PluginInstallPlanView/Envelope, PluginAuditLogList): install-plan, auth-session start/get/inspect, audit tool-calls/events. Only the OAuth HTML callback + { error } envelopes remain bare. install-dialog unwraps res.data for the auth-session methods. zai-org#4 /auth/me GET+PUT → sendData(AuthMeViewSchema, { user, session }) → { data }. New shared schemas/auth.ts (UserProfileView/AuthSessionSummaryView/AuthMeView). web lib/api.ts getMe/updateMe + lib/server-auth.ts read res.data.{user,session}. zai-org#5 service→presenter retreat (controller owns presentation) across 11 app-facing modules (organization/files/relationship/memory/remote-agents/workspace/ workspace-apps/automation/chat/model-groups/mcp-plugins): service.ts now returns DOMAIN RECORDS; the controller calls present*() before each send (envelopes unchanged). present*() in service.ts: 106 → 65 (remainder are nested-record assembly + cross-module-consumed records + Tier-D timestamp wrappers, permitted by the layer-matrix JUDGMENT clause). New record types in repo.types/presenter per module. Fixed 2 new r7 (return { ...row }) violations the retreat introduced (remote-agents/workspace-apps) via const-assignment. Tier-D / non-HTTP modules (session/context/tasks/tool-call-tasks) excluded per plan ("不强制 presenter"). Verification: full monorepo build green; api 1640 pass/0 fail/2 skip; guards (layering/db/fk/soft-delete/datetime) clean; device-sdk 8/8, device-runtime 314/0fail, remote-agent-daemon 24/24; web + mobile typecheck clean.
JoshZiel83
pushed a commit
to JoshZiel83/Synapse
that referenced
this pull request
Sep 19, 2026
… guard close Round-4 review: cloud-input schema stricter than server; relationship/remote-agents controllers still bare; access-bindings half-migrated; TableRow leaked outside repo files (guard couldn't prove repo-only DB access); §5.3 split-controller/appRoute mechanism + guard rule 5 entirely unadopted across mixed modules. zai-org#1 CreateCloudDeviceInputSchema: title/hostProvider → optional (the server applies defaults; the strict schema would reject requests the server accepts). §5.3 (subsumes zai-org#2 + zai-org#3) — adopt appRoute/wireRoute across ALL mixed modules: implemented guard rule r5 (mixed modules forbid bare app.<verb>; must use appRoute/wireRoute/split-controller). Migrated every bare route in devices, mcp-plugins, runtime-authorizations, files, relationship, remote-agents, automation, im — APP routes → appRoute (handler returns value; helper wraps via sendData → { data }), WIRE (handshake/webhook/binary/HTML/WSS/reverse-MCP) → wireRoute (bare). devices/mcp-plugins were pure registration swaps (already sendData, no envelope change); the bare modules went bare→{data} with new shared View schemas (automation/files/im/relationship/remote-agents/runtime- authorizations.ts) + web/mobile consumer unwraps in lockstep. Guard r5 baseline now 0. Fixed appRoute to PRESERVE a handler-set status (reply.status(201)) — the prior code let sendData's default 200 clobber 201s; added route.test.ts locking the { data }/201/204/wire contract. zai-org#3 access-bindings GET query → camelCase (subjectKind/subjectActorId/…); route already on appRoute + { data } response. zai-org#5 repo-boundary: extended guard r2 from service/controller-only to ALL non-repo files (only repo*.ts/repo.types.ts may use TableRow). Moved the TableRow/ TableInsert/TableUpdate aliases out of 12 helper files (access/binding-storage + subject-registry, im/service/*, mcp-plugins/{plugin-auth-connections,mijia}, memory/indexing, session/runtime, tasks/action-tokens) into per-module repo.types.ts. guard r2 baseline now 0. Also: guard now strips comments before matching (kills doc-comment false positives). GUARD-LAYERING BASELINE IS NOW FULLY EMPTY (0 grandfathered entries across all 6 rules: r1/r2/r3/r4/r5/r7) — the layering is genuinely enforced, not just passing. zai-org#4 (service→presenter) was completed per-plan in round-3; the 64 remaining present* are the JUDGMENT-permitted nested-record/cross-module/Tier-D cases. zai-org#6 (web runtime schema.parse) not required by plan (device-sdk is the parsing consumer); web stays a typed thin client. Verification: full monorepo build green; api 1646 pass/0 fail/2 skip (+route.test); guards (layering[empty]/db/fk/soft-delete/datetime) clean; device-sdk 8/8, remote-agent-daemon 24/24; web + mobile typecheck clean.
JoshZiel83
pushed a commit
to JoshZiel83/Synapse
that referenced
this pull request
Sep 19, 2026
…in AGENTS.md Round-5 review re-flagged 4 items; an exhaustive re-audit shows 3 are already resolved and 1 is at its intended end-state: zai-org#1 CreateCloudDeviceInputSchema strictness — STALE: fixed in round-4 (fddca43); title/hostProvider are .optional() and device-sdk parses the same schema it sends, so SDK and server accept identical bodies. Verified, no change needed. zai-org#3 TableRow outside repo — RESOLVED in round-4: the guard r2 baseline is empty ([]); all DB-row types live in repo*.ts/repo.types.ts; the guard now strips comments so doc-comment mentions aren't flagged. The reviewer's "few left, mostly comments" predates that. No change needed. zai-org#4 web runtime schema.parse — OUT OF PLAN SCOPE: the master plan classifies web-next as an app client and device-sdk + the API's sendData/appRoute as the runtime-parse boundary. web is a typed compile-time-contract client by design; the 6 remaining snake reads are RFC8628 OAuth device-flow wire fields, and the 3 any are memory request-body accumulators (no shared input schema). No change. zai-org#2 service→present* (64 sites) — ALL PERMITTED: a fresh per-site classification of every present*() in service.ts found exactly 0 clean violations. Each is one of: (a) a row→record / record-assembly mapper producing an in-api XxxRecord (e.g. presentFileAsset→StoredFileRecord, presentActorRow→Actor, presentUser→User), (b) a thin presentInstant/presentOptionalInstant timestamp wrapper, or (c) a record consumed cross-module (automation rules read by ai/session-tools; remote-agents snapshot read by chat; etc.). The lone HTTP-body case (model-groups getModelGroup→ModelGroupDetailView) is also consumed in-api as a record by the seeder + ownership pre-handlers + updateModelGroup, so it's the same cross-consumed-record exception. The master-plan JUDGMENT clause + Tier-D exemption permit all 64. To stop future reviews re-flagging this as "unfinished", codified the rule in AGENTS.md (七层职责): service may call present* for row→record assembly, timestamp wrapping, and cross-module records; what's forbidden is service producing+bare-returning the OUTWARD app DTO as the sole HTTP-body source — that stays in the controller via appRoute/sendData. Verification: full build green; api 1646 pass/0 fail/2 skip; guards (layering[empty]/datetime) clean; device-sdk 8/8; web + mobile typecheck clean. No functional code change → running containers already on the verified r4 state.
JoshZiel83
pushed a commit
to JoshZiel83/Synapse
that referenced
this pull request
Sep 19, 2026
…(structural, zero-behavior-change) External review R4 zai-org#2 (full abstraction generalization), structural phase. 'add a provider = implement one interface + register.' No behavior change; the new behavioral seams are inert stubs the next sub-phases fill. - Full SandboxAdapter interface: meta (SandboxAdapterMeta) + endpoint (AdapterEndpointContract) + ready() + workingSet() + async rebuildDataPlane + reconnectDataPlane + listOrphans/destroyResource. Drops the closed 'kind' field. - Config-free metadata leaf (adapter-keys.ts -> adapter-metadata.ts): a table of {key, provider, mode, meta{tag,offBox,credentialed,config{envKeys,validate,validateProduction}}, endpoint{scheme,build,identityOk}}. SANDBOX_ADAPTER_KEYS derives from it; importable by both config + registry (RawEnv type-only). - Collapse the closed SandboxBackendKind union: deleted the union + every 'kind' field; sandboxSpecVolumeSubpath / pid-identity -> provider:string. No switch(kind)/assertNever existed -> no exhaustiveness lost. - Config fold: the per-provider superRefine blocks -> meta.config.validate(env) (+ validateProduction, currently inert). Behavior byte-identical. - Dispatch collapse (R3.2/R3.3/R3.7 PRESERVED): bareTargetIdentityOk resolves the identity predicate FROM THE LEAF by tag (unknown -> clean false deny, never adapterForRow which throws); rebuildBarePlane is async + awaited inside try/catch so a throw returns a clean runtime_constraint deny instead of escaping the catch-less callers; the gen-fence + tombstone re-check stay in the one synchronous span before the register. - ready() replaces the catalogSource fork (resident: waitForCatalog+waitForTunnelEndpoint via injected waiters, both budgets kept; bare: ok:true). Discriminated SandboxSpec (Host|OffBox) over spec.host!. Gates: typecheck 0, sandbox 204/0, fail-closed 16/16, config 21/21, guards clean, verify:boundary green (api 2851/0).
JoshZiel83
pushed a commit
to JoshZiel83/Synapse
that referenced
this pull request
Sep 19, 2026
…eprovision + provision race/deadline External review R5 zai-org#2 (P0 data) + zai-org#7 (P1 lifecycle). User chose Option B for zai-org#2. zai-org#2 = Option B (off-box VM not reused across turns). A reused cube VM diverges from the host mirror between turns (background writers + no host-side conflict detection), and the per-turn refresh/commit only touch the host mirror — so any naive per-turn PUSH/PULL would clobber uncommitted VM work (design review). Instead: DECLINE the fast-path for off-box, so each off-box turn does a clean teardown (PULL VM->mirror + commit) + cold reprovision (re-materialize head + replicate-only PUSH). Convergence is automatic via the existing provision/teardown primitives; no shared-STORAGE_DIR hot-path dependency, no stat-cache thrash. Host bare/resident keep VM/process reuse (their plane IS the host mirror). zai-org#7 provision double-create + late deadline: - Early deadline: the mint now writes sandboxes.deadline_at = NOW()+5min in the SAME tx, BEFORE create() returns, so a provision that HANGS after mint (VM up, readiness never completes) is visible to reapStuckProvisioningSandboxes. The post-create back-fill refreshes. - Back-off guard: provisionSandbox is called inside the Redis session lock; the only same-session overlap is a lock-expiry window where a 2nd worker enters mid-provision. Design review rejected a pg_advisory_xact_lock (single-tx breaks staged crash-recovery + pins a pool conn ~60s + nested-call deadlock). Instead: when the existing mounts belong to a genuinely in-flight provision (a 'provisioning' row still within its deadline), FAIL RETRYABLE (409) rather than tearing down the other worker's fresh VM (orphan + failed provision). A crashed/stale provision (past deadline) or an 'active' prior turn falls through to the teardown. SandboxRow gains deadlineAt. Gates: typecheck 0, verify:boundary green (2905/0), 25/25 on provision/reaper/mint/consume suites.
JoshZiel83
pushed a commit
to JoshZiel83/Synapse
that referenced
this pull request
Sep 19, 2026
…rbitrary host write)
R6 review P0: CAS materialize() recreates a base-snapshot symlink as a REAL host
symlink inside the off-box mirror; the R5 assertUnderMirror belt is a purely
LEXICAL resolve()+prefix check (zero fs access), so the teardown PULL's mkdir /
writeFile FOLLOW the symlink and write OUTSIDE the mirror as the shared API
process (reviewer reproduced {"escaped":"PWN"}). Reachable cross-backend: a
docker/device turn can commit `ln -s /etc evil` into the shared base snapshot.
Fix — no-follow the mirror write legs (design-reviewed: neutralize, don't
preserve; keep walkMirror symlink-free):
- firstSymlinkComponentUnderMirror: walk the target's components UNDER the mount
top-down with lstat, stopping at (returning) the first symlink — so a symlink
at ANY depth is detected without ever following one (lstat only no-follows its
last component; returning at the first symlink guarantees every lstat'd path
has only real-dir ancestors).
- writeMirrorFile / makeMirrorDir: neutralizeMirrorSymlink UNLINKs the first
symlink component before mkdir/writeFile, so the write lands on a REAL node
inside the mount — the actor's bytes stay inside the mount (no legit data loss)
and the weaponized symlink is dropped (off-box can't round-trip symlinks anyway,
H-7); unlinking it before scanCommitDir also purges it from the committed tree
rather than re-recording the escape primitive.
- readMirrorFile: assertNoSymlinkOnReadPath refuses to read THROUGH a symlink (a
host-file exfil into the VM/CAS).
- walkMirror is UNCHANGED (it is shared by PUSH's listMirrorFiles — emitting
symlinks would either abort provision or stream a host file into the VM); it
already skips symlink dirents, so PUSH never reads through one.
The write-time unlink + the zai-org#2 refreshSpaces gate (Phase C, removing the only
mid-turn mirror materializer) mean the sole symlink source is the pre-PULL base
materialize, so the sequential single-threaded writes have no concurrent
symlink-inserter to race (openat2 via fs-helper is the deferred gold standard).
Tests: reproduce the escape → the VM file lands INSIDE the mirror (symlink
replaced by a real dir), nothing written to the symlink target; PUSH never
exfiltrates the host secret behind a mirror symlink. verify:boundary green.
JoshZiel83
pushed a commit
to JoshZiel83/Synapse
that referenced
this pull request
Sep 19, 2026
… (no silent overwrite) R6 review P1: refreshSpaces was off-box-UNAWARE. For an off-box mount it advanced base_snapshot_id to a concurrently-committed head H2 and synced only the host mirror, but NEVER re-pushed the remote VM (which still held the pushed snapshot H). At teardown the PULL brought the VM (stale x=A) into the mirror and committed against base=H2 → x=A looked like a fresh LOCAL edit → silently overwrote another actor's committed x=B, with no conflict surfaced. zai-org#2=Option B (per-turn cold reprovision) did not touch refreshSpaces, so the race persisted. Fix (design-reviewed — SOUND): make refreshSpaces off-box-aware. - sessionSandboxIsOffBox(sessionId): resolve the session's current sandbox via getSandboxBySessionForControl → adapterForRow().meta.offBox (any lookup failure ⇒ false ⇒ the pre-R6 host behavior). - For an off-box session, refreshSpaces returns early (empty) — NO base-advance, NO mirror head-apply, sync never called. base stays at the pushed snapshot H, so the teardown pull→commit performs a correct 3-way merge (base=H, latest=Hn, working=VM) that surfaces the concurrent change as a REAL conflict + sidecar instead of a silent overwrite. (We do NOT re-push the refreshed head into the VM — that would clobber the actor's uncommitted VM work, the reason Option B avoids per-turn push.) Host / no-sandbox sessions are unchanged (one extra sandbox lookup per refresh). Test: an off-box session's refresh is a no-op (sync never invoked, empty result). verify:boundary green (sandbox 303/303).
JoshZiel83
pushed a commit
to JoshZiel83/Synapse
that referenced
this pull request
Sep 19, 2026
…the tolerant coercion codec R7 clean-slate (finding zai-org#2). pending-conflicts.ts hand-rolled a DEFENSIVE COERCION that tolerated old/partial data: normalizeSidecarRef defaulted a missing `kind` to "file", per-field type-guards silently blanked/dropped bad fields, and gc.ts kept a SECOND tolerant raw-walk (collectFromSidecarMap) of the same two JSONB blobs. We own the write shape (no old data), so this is pure maintenance burden. Fix (mirrors decodeSandboxCapabilityDescriptor's strict-decode convention): - Replace normalizePendingConflicts/normalizePendingRefresh with decodePendingConflicts/ decodePendingRefresh — strict Zod schemas, required original/sidecar/kind (no kind→"file" default), single top-level safeParse→{} fail-SAFE (a corrupt blob never crashes a turn, but there is NO per-field old-data coercion). - Sidecar schema is a PLAIN object (kind: z.string(), not an enum): contentSha/target stay OPTIONAL because they are kind-discriminated RUNTIME states (a file with no contentSha / symlink with no target is a genuinely-irrecoverable sidecar, surfaced as PERMANENT by isSidecarPayloadIrrecoverable — not old data), and an unrecognized kind is decoded + handled rather than nuking the whole blob. - Fold gc.ts's second reader into the SAME strict decode (decode both stores, collect each FILE-kind sidecar's contentSha as a GC root) — one codec, not two. - Delete the dead isRecord/stringArray/normalizeSidecarRef helpers; repoint repo.ts + the service barrel; rewrite the pre-round/coercion tests to the strict semantics (a malformed blob → {} entirely, no silent per-field default). verify:boundary green (sandbox+cube 316/316; pending/refresh/gc 19/19).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Imports two upstream branches from the PekingSpades fork as squashed commits.
feat/relay-vfs-dom-semantic-fuse(4 source commits, squashed)Source: https://github.com/PekingSpades/synapse/tree/feat/relay-vfs-dom-semantic-fuse
fix/relay-auto-skill-discovery-order(1 source commit, squashed)Source: https://github.com/PekingSpades/synapse/tree/fix/relay-auto-skill-discovery-order
Test plan
npm run buildnpm run format:check