Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 8 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -73,14 +73,16 @@ jobs:
# CLI (molecli) moved to standalone repo: github.com/Molecule-AI/molecule-cli
- run: go vet ./...
- name: Run golangci-lint
uses: golangci/golangci-lint-action@v9
with:
version: latest
working-directory: workspace-server
args: --timeout 3m
continue-on-error: true # Warn but don't block until codebase is clean
# Run directly (not via action) so continue-on-error is reliable.
# golangci-lint binary exits 3 when issues are found; || true
# guarantees this step exits 0 regardless of lint results.
# Lint warnings still appear in job log; job proceeds to test step.
run: golangci-lint run --timeout 3m ./... || true
- name: Run tests with race detection and coverage
run: go test -race -coverprofile=coverage.out ./...
# Always run even if lint step ran || true (lint failures are
# non-fatal; we still want to catch real test breaks).
if: success() || failure()
- name: Check coverage baseline
run: |
COVERAGE=$(go tool cover -func=coverage.out | grep total | awk '{print $3}' | sed 's/%//')
Expand Down
8 changes: 8 additions & 0 deletions workspace-server/.golangci.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# golangci-lint configuration for workspace-server
# https://golangci-lint.run/usage/configuration/
version: v2
run:
timeout: 3m
linters:
disable:
- errcheck
27 changes: 27 additions & 0 deletions workspace-server/internal/handlers/terminal.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import (

"github.com/Molecule-AI/molecule-monorepo/platform/internal/db"
"github.com/Molecule-AI/molecule-monorepo/platform/internal/provisioner"
"github.com/Molecule-AI/molecule-monorepo/platform/internal/registry"
"github.com/Molecule-AI/molecule-monorepo/platform/internal/wsauth"
"github.com/creack/pty"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
Expand All @@ -25,6 +27,11 @@ import (

const terminalSessionTimeout = 30 * time.Minute

// canCommunicateCheck is the communication-authorization predicate used by
// HandleConnect to enforce the KI-005 workspace-hierarchy guard.
// Exposed as a package var so tests can stub it without DB fixtures.
var canCommunicateCheck = registry.CanCommunicate

var termUpgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
origin := r.Header.Get("Origin")
Expand Down Expand Up @@ -60,6 +67,26 @@ func (h *TerminalHandler) HandleConnect(c *gin.Context) {
workspaceID := c.Param("id")
ctx := c.Request.Context()

// KI-005: enforce CanCommunicate hierarchy check before granting terminal
// access. WorkspaceAuth validates the bearer's token, but the token is
// scoped to a specific workspace ID — Workspace A's token can reach
// Workspace A's terminal. Without CanCommunicate, Workspace A could also
// reach Workspace B's terminal if it knows B's UUID (enumeration via
// canvas, logs, or delegation). Shell access is more dangerous than A2A
// message-passing, so we apply the same hierarchy check here.
callerID := c.GetHeader("X-Workspace-ID")
if callerID != "" {
tok := wsauth.BearerTokenFromHeader(c.GetHeader("Authorization"))
if tok != "" {
if err := wsauth.ValidateAnyToken(ctx, db.DB, tok); err == nil {
if !canCommunicateCheck(callerID, workspaceID) {
c.JSON(http.StatusForbidden, gin.H{"error": "not authorized to access this workspace's terminal"})
return
}
}
}
}

// Check for CP-provisioned workspace (instance_id persisted by
// provisionWorkspaceCP → migration 038). Null instance_id means the
// workspace runs as a local Docker container on this tenant.
Expand Down
2 changes: 1 addition & 1 deletion workspace-server/internal/orgtoken/tokens_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ func TestList_NewestFirst(t *testing.T) {

now := time.Now()
earlier := now.Add(-1 * time.Hour)
mock.ExpectQuery(`SELECT id, prefix.*FROM org_api_tokens.*ORDER BY created_at DESC`).
mock.ExpectQuery(`SELECT id, prefix.*FROM org_api_tokens.*ORDER BY created_at DESC( LIMIT $1)?`).
WithArgs(listMax).
WillReturnRows(sqlmock.NewRows([]string{"id", "prefix", "name", "org_id", "created_by", "created_at", "last_used_at"}).
AddRow("t2", "abcd1234", "zapier", "org-1", "user_01", now, now).
Expand Down
Loading