Add opt-in hardening to the agents-as-a-service example:
- commented runtimeClassName/nodeSelector fields for sandboxes and Deployments
- optional sandbox NetworkPolicy plus exposure-variant policies
- optional dsh-provider-keys-extra Secret for extra provider keys
- docs/optional-hardening.md with verification, caveats and rollback
The base walkthrough stays portable; no gateway code, routes or auth changed.
Co-authored-by: SREDevOpsAgents <agents@sredevops.org>
Add the project-local dsh-agent-sandbox-platform skill covering platform operations, the optional agent-sandbox MCP server, feature-extension recipes, and a read-only preflight. Document project skills in README, docs and AGENTS.md.
Co-authored-by: SREDevOpsAgents <agents@sredevops.org>
Updated the release workflow to source environment variables from versions.env and modified the tag comparison logic.
Signed-off-by: Nicolás Georger <info@sredevops.org>
Update the script to read environment variables from versions.env using 'source' instead of a loop.
Signed-off-by: Nicolás Georger <info@sredevops.org>
* 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.
* docs(changelog): generate initial changelog from history
* fix(scripts): make set-version update Dockerfile and sample revision pins
* build(hadolint): document intentional lint exceptions for the main image
---------
Co-authored-by: SREDevOpsAgents <agents@sredevops.org>
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>
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>
oauth2-proxy base64-decodes cookie_secret and requires it to decode to 16/24/32
bytes. Plain 'openssl rand -base64 32' yields 44 chars of standard base64
(padding + possibly '+/'), which it cannot decode and treats as 44 raw bytes,
failing with 'cookie_secret must be 16, 24, or 32 bytes ... but is 44 bytes'.
Use URL-safe, unpadded base64 instead (secrets.token_urlsafe(32) or the
openssl|tr variant).
Co-authored-by: SREDevOpsAgents <agents@sredevops.org>
Add Gateway and HTTPRoute manifests to route external traffic to the
dsh-gateway service (dsh-demo:8080).
- Gateway dsh-gateway: HTTPS listener with TLS termination against a
dsh-demo-tls cert secret.
- HTTPRoute dsh-gateway: hostname -> dsh-gateway:8080.
- Example TLS secret manifest + README with apply/verify/rollback.
Co-authored-by: SREDevOpsAgents <agents@sredevops.org>
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>
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>
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>
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>
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>
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>
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>
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>
* 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>
Added a step to create a manifest list for Docker images built for different platforms without annotations if the previous step fails.
Signed-off-by: Nicolás Georger <info@sredevops.org>
- Enhanced Dockerfile with updated image labels and source.
- Revised README to reflect changes in DSH Web architecture and deployment instructions.
- Added Kubernetes sandbox template for DSH Web with security context and environment variables.
- Updated .env.sample and CODEOWNERS for consistency.
- Introduced multi-arch Docker build workflow for improved CI/CD.