1
0
mirror of https://github.com/sredevopsorg/deepseek-kubernetes-sandbox.git synced 2026-11-03 01:13:24 +00:00
Commit Graph

14 Commits

Author SHA1 Message Date
SREDevOpsAgents
e6e109d9a9 feat(repo)!: restructure around the dsh-harness image and one example per implementation
- the root Dockerfile is the primary artifact; versions.env is the single source of truth
- merge gateway-api/dsh-demo into examples/dsh-agents-as-a-service (manifests/, exposure variants)
- move the single-tenant template into examples/minimal-single-tenant
- publish the gateway as a nested package under the repository image
- mirror the DSH ref for release identities (first release dsh-v0.2.1-rc.2-r2)
- CI: lint/python/manifests/gateway-image, reusable multi-arch build, tag-driven release, weekly e2e
- docs: README, docs/{main-image,examples,gitflow,versioning}, example READMEs, AGENTS.md, governance
- SSO is opt-in again; gateway pull policy and cookie defaults match the documented demo

BREAKING CHANGE: example manifest paths, the gateway image name, and the removal
of deploy/ and gateway-api/ affect existing deployments; see the migration table
in README.md.
2026-10-25 06:37:28 -03:00
Nicolás Georger
53e55fc851 Modify gateway.yaml for image policy and timeout values
Updated imagePullPolicy to always pull the latest image and increased timeout values for IDLE_TIMEOUT and WAKE_TIMEOUT.

Signed-off-by: Nicolás Georger <info@sredevops.org>
2026-10-25 04:41:00 -03:00
SREDevOps.org Agent
ed1398dfd1 feat(gateway): SSO landing auto-connects and offers recreate sandbox (#15)
After oauth2-proxy authentication, the gateway now renders a minimal SSO
landing (no signup/token forms) that auto-connects to the sandbox and offers
a single 'Recreate sandbox' action for a clean start.

- _sso_session: stop blocking on wait_ready (provisioning is non-blocking;
  the proxy path still awaits readiness), so the landing reports
  'Provisioning' instead of stalling for WAKE_TIMEOUT
- landing: SSO branch renders a minimal page (signed-in user + sandbox state
  + auto-connect meta-refresh), replacing the generic signup/token forms
- new POST /recreate: deletes the claim (cascade pod+PVC), waits out
  finalizers, re-provisions, and reconnects; CSRF-protected, SSO-only
- tests: 109 passing (SSO landing provision/connect/recreate cases)

Co-authored-by: SREDevOpsAgents <agents@sredevops.org>
2026-10-25 04:34:13 -03:00
SREDevOps.org Agent
3f46279b39 feat(gateway-api): GitHub SSO via oauth2-proxy in front of dsh-gateway (#13)
Add oauth2-proxy (GitHub provider, org-restricted) as a reverse proxy in
front of the dsh-gateway, wired through the Traefik Gateway API. The gateway
inherits the authenticated GitHub username via X-Forwarded-User (SSO).

- gateway.py: opt-in AUTH_PROXY_HEADER trust, remote_user(), SSO bootstrap
  (auto-provision claim + mint/rotate token + session cookie), patch_token()
- 50-gateway.yaml: AUTH_PROXY_HEADER=X-Forwarded-User, COOKIE_SECURE=true
- gateway-api/dsh-demo: oauth2-proxy Deployment/Service + secret example,
  HTTPRoute backend -> oauth2-proxy:4180
- tests: 10 new SSO cases (106 passing)

Placeholders (dsh-demo.example.com, your-github-org) are substituted at
apply time.

Co-authored-by: SREDevOpsAgents <agents@sredevops.org>
2026-10-25 03:43:32 -03:00
SREDevOps.org Agent
0a61e57db4 Handshake under the pod-IP authority for WebSocket upgrades (#11)
DSH binds its auth cookie to the request authority (payload.authority ===
authority) and looks the cookie up under a name derived from the Host. The
upgrade presents Host = pod-IP:3080 (websockets derives it from the URI), so a
cookie minted for the browser authority is rejected with 401. The previous
pod-IP change fixed the Host fence (403) but not this, so the client kept
looping on "[connection] connection lost, retry #N".

Perform the WS handshake under the pod-IP authority too, so the cookie matches
the Host the upgrade carries. The HTTP path keeps the browser authority.

Also serialise the lazy Kubernetes client init: anyio.to_thread can enter it
concurrently, and a thread that observes _crd set while _core is still None
returns a None client. The live log showed the result:

  AttributeError: 'NoneType' object has no attribute 'read_namespaced_pod'

Co-authored-by: SREDevOpsAgents <agents@sredevops.org>
2026-10-21 15:29:22 -03:00
SREDevOps.org Agent
abe5b4d5bb Dial the sandbox pod IP for WebSocket upgrades (#10)
DSH's Host fence (isTrustedApiRequest) only trusts loopback, an allow-listed
host, or its own bind address, and requires any Origin to match the Host
authority. The gateway forwarded the browser Host as an additional header,
but websockets derives Host from the URI and cannot be overridden, so the
request carried a duplicate/mismatched Host plus the browser Origin and DSH
answered 403. The gateway then closed with 1011, which the DSH client retries
forever:

  [connection] connection lost, retry #1 ... #55

Dial the pod IP instead - it is the bind address, so the derived Host passes
the fence - and forward no Origin; CSWSH is already rejected by same_origin()
before the upstream connection. Fall back to the service FQDN when
status.podIPs is empty and bracket IPv6 literals.

Reproduced against dsh-v0.2.1-rc.2: pod-IP upgrade with no Origin opens;
the same upgrade carrying the browser Origin gets HTTP 403.

Co-authored-by: SREDevOpsAgents <agents@sredevops.org>
2026-10-21 15:09:51 -03:00
SREDevOps.org Agent
163abaeac0 Do not read-time-out proxied upstream streams (#9)
Every proxied response is streamed through _relay using one shared
httpx.AsyncClient configured with timeout=HTTP_TIMEOUT (300s). An idle SSE
or long-poll stream therefore raised httpx.ReadTimeout every five minutes,
which escaped the StreamingResponse as a full traceback and truncated the
browser's stream.

Build proxied requests with a timeout that leaves read unbounded (connect,
write and pool stay bounded), and end the relay quietly on httpx.HTTPError
instead of propagating it. The relay is cancelled when the browser
disconnects and _relay closes the upstream response, so an unbounded read
cannot outlive the client.

Docs: note the split in the HTTP_TIMEOUT row.

Co-authored-by: SREDevOpsAgents <agents@sredevops.org>
2026-10-21 14:49:55 -03:00
SREDevOps.org Agent
687640621e Forward the upstream query byte-for-byte (#8)
filtered_query() round-tripped the query through parse_qsl/urlencode, which
percent-encoded DSH's Vite-style combined-module URLs. The client asks for
"plugins/??@deepseek-ai/dsh-client-modules/client.js&rev=..." - the raw query
itself starts with "?" - so the gateway sent "%3F%40deepseek-ai..." and the
upstream answered 404. The browser then failed the boot with:

  client-modules: HTML did not preload
  @deepseek-ai/dsh-client-modules/client.js

Strip only token parameters and keep every other raw part untouched. Add
regression tests for the combined-module URL and an encoded token key.

Co-authored-by: SREDevOpsAgents <agents@sredevops.org>
2026-10-21 14:24:01 -03:00
SREDevOps.org Agent
9dea4a7f67 Normalise the pod model returned by CoreV1Api (#7)
get_pod() returned kubernetes' V1Pod model, but dsh_cookie() indexes it as a
dict (pod.get("metadata")), so every gateway handshake crashed with:

  AttributeError: 'V1Pod' object has no attribute 'get'

and every proxied request answered 500. The unit tests inject a fake that
returns dicts, so the mismatch only appeared against a real API server.

Normalise at the adapter boundary with ApiClient().sanitize_for_serialization
and cover it with a test that returns a real V1Pod.

Also document digest-pinned gateway rollouts: the workflow publishes the
mutable :gateway tag, and imagePullPolicy: IfNotPresent keeps the cached old
image across kubectl apply / rollout restart.

Co-authored-by: SREDevOpsAgents <agents@sredevops.org>
2026-10-21 14:11:40 -03:00
SREDevOps.org Agent
c193ed21a4 Add a startup probe so slow DSH cold starts are not killed (#6)
DSH takes tens of seconds to load plugins and start serving on a small
node, and every resume starts cold. The liveness budget (initialDelay
15s, period 15s, failureThreshold 3) is shorter than that, so the kubelet
killed the container mid-startup. DSH exits 0 on SIGTERM, which the
sandbox controller records as PodSucceeded/Finished - the pod is then
never recreated and the sandbox stays unusable until a manual
suspend/resume cycle.

Gate liveness and readiness behind a startupProbe on /favicon.svg with a
120s budget (24 x 5s, matching the gateway's WAKE_TIMEOUT). Liveness still
catches a genuinely hung server once startup succeeds.

Co-authored-by: SREDevOpsAgents <agents@sredevops.org>
2026-10-21 13:55:28 -03:00
SREDevOps.org Agent
a8ef211aa4 Fix DSH crash-loop after suspend/resume (fsGroup chmod) (#5)
Resuming a sandbox recreates its pod on the same PVC. The kubelet's default
fsGroup policy ("Always") recursively ORs 0660 into every file on each pod
start, so DSH's .credentials.yaml — created 0600 — comes back as 0660. The
credentials plugin refuses to boot in that case:

  credentials-local: .../.credentials.yaml is readable beyond its owner
  (mode 660); run "chmod 600 ..."

That makes the first resume after a suspend crash-loop, which breaks the
example's step 6 and any gateway wake-on-connect.

Set fsGroupChangePolicy: OnRootMismatch on both sandbox templates. The
kubelet then skips the recursive walk when the volume root already carries
the fsGroup ownership/mode applied on the first mount — exactly the
suspend/resume case, while a fresh PVC is still chowned correctly.

Document the upgrade path: Sandbox objects snapshot their pod template, so
existing sandboxes and warm-pool members need the policy patched (plus a
one-time chmod 600) or must be recreated.

Co-authored-by: SREDevOpsAgents <agents@sredevops.org>
2026-10-21 13:40:47 -03:00
SREDevOps.org Agent
29eefd6ae5 Fix idle sweeper suspending claims the gateway does not own (#4)
The sweeper listed every SandboxClaim in NAMESPACE and treated each
dsh-<user> name as one of its own users. A claim applied by hand from
the resource-layer manifests (e.g. 30-claim-alice.yaml) therefore looked
exactly like a gateway user: the idle clock started on first sighting
and the pod was suspended after IDLE_TIMEOUT. That deletes the pod the
README's step 3 logs/port-forward targets, so the walkthrough broke as
soon as the gateway was deployed next to it.

Mark claims created by create_claim() with
aaas.example.com/managed-by=dsh-gateway and skip everything else in
sweep_once() before any idle bookkeeping happens. Document the
ownership boundary in the example docs, the manifest and ADR-5, add
unit coverage for the marker and the skipped sweep, and assert in
run-test-kind.sh that alice's hand-applied claim stays Running while
bob is correctly idle-suspended.

Co-authored-by: SREDevOpsAgents <agents@sredevops.org>
2026-10-21 13:27:19 -03:00
SREDevOps.org Agent
6301c21bea Make DSH agents-as-a-service browser-usable; fix probe crash-loop (#3)
* Rewrite DSH gateway for browser sessions, HTTP and WebSocket

- Replace the Flask proxy with a Starlette ASGI app (uvicorn factory),
  keeping the control plane (claims, wake/suspend, idle sweeper).
- Add HttpOnly gateway sessions and a server-side DSH launch-token
  handshake: read the token from pod logs, exchange it for a dsh-auth
  cookie, cache per (user, pod UID, Host), refresh once on 401.
- Proxy HTTP/SSE and WebSocket (/api/remote.mux) under /u/<user>/, with
  header stripping/injection, Location rewriting and wake-on-connect.
- Add a landing/signup/login page, health endpoint, pinned dependencies,
  a read-only-rootfs gateway image, 87 cluster-free tests and docs.

* Fix DSH probe crash-loop and harden gateway manifests

- Probe /favicon.svg instead of /: DSH gates / behind its launch-token
  cookie and returns 401, which made warm-pool pods crash-loop.
- Pass DSH_TRUSTED_HOSTS through the entrypoint as --trusted-host args
  for public/ingress hosts; keep localhost trusted.
- Deploy the published :gateway image, add pods/pods-log RBAC, /healthz
  probes, read-only rootfs with a /tmp emptyDir and non-root security.
- Add optional Traefik Ingress and document the prerequisites.

* Update CI, smoke tests, build tooling and docs

- Run gateway pytest/ruff and build/smoke-test the image in CI.
- Parameterize the DSH build version and pnpm version inputs.
- Exercise cookie signup, HTML proxy, WebSocket auth, suspend/wake and
  delete in run-test-kind.sh.
- Add Makefile/build.sh targets and document the architecture, handshake,
  probes, trusted hosts, troubleshooting and limitations.

---------

Co-authored-by: SREDevOpsAgents <agents@sredevops.org>
2026-10-21 12:10:03 -03:00
SREDevOps.org Agent
98cb547ae5 Add DeepSeek Harness Web agents-as-a-service platform pattern (#2)
Port the upstream hermes-agents-as-a-service example to DSH Web:
numbered manifests (00-prereqs .. 50-gateway), a Python gateway
(signup, per-user tokens, wake-on-connect proxy, idle suspend),
run-test-kind.sh smoke test, and docs. Fix deploy/k8s/sandbox-template.yaml
(v1beta1, correct image, volumeClaimTemplates). Add a validate workflow
(manifest server dry-run + gateway unit tests).

Co-authored-by: SREDevOpsAgents <agents@sredevops.org>
2026-10-21 09:45:05 -03:00