Compare commits

...

4 Commits

Author SHA1 Message Date
core-be ce542cb265 fix(provisioner): fix collectCPConfigFiles return values
Block internal-flavored paths / Block forbidden paths (pull_request) Successful in 7s
E2E Staging SaaS (full lifecycle) / E2E Staging SaaS (pull_request) Has been skipped
CI / Detect changes (pull_request) Successful in 16s
E2E Staging Canvas (Playwright) / detect-changes (pull_request) Successful in 19s
E2E API Smoke Test / detect-changes (pull_request) Successful in 19s
Harness Replays / detect-changes (pull_request) Successful in 14s
Handlers Postgres Integration / detect-changes (pull_request) Successful in 22s
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 27s
E2E Staging SaaS (full lifecycle) / pr-validate (pull_request) Successful in 51s
Runtime PR-Built Compatibility / detect-changes (pull_request) Successful in 37s
qa-review / approved (pull_request) Failing after 28s
gate-check-v3 / gate-check (pull_request) Failing after 39s
sop-checklist / na-declarations (pull_request) awaiting /sop-n/a declaration for: qa-review, security-review
security-review / approved (pull_request) Failing after 30s
sop-tier-check / tier-check (pull_request) Successful in 20s
lint-required-no-paths / lint-required-no-paths (pull_request) Successful in 1m21s
CI / Shellcheck (E2E scripts) (pull_request) Successful in 18s
CI / Python Lint & Test (pull_request) Successful in 28s
Runtime PR-Built Compatibility / PR-built wheel + import smoke (pull_request) Successful in 18s
E2E Staging External Runtime / E2E Staging External Runtime (pull_request) Successful in 5m25s
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Successful in 7m4s
E2E API Smoke Test / E2E API Smoke Test (pull_request) Failing after 11m53s
E2E Staging Canvas (Playwright) / Canvas tabs E2E (pull_request) Failing after 11m41s
Harness Replays / Harness Replays (pull_request) Failing after 11m36s
CI / Canvas (Next.js) (pull_request) Successful in 18m30s
CI / Canvas Deploy Reminder (pull_request) Has been skipped
CI / Platform (Go) (pull_request) Successful in 19m52s
CI / all-required (pull_request) Successful in 6s
sop-checklist / all-items-acked (pull_request) acked: 7/7
audit-force-merge / audit (pull_request) Has been skipped
Lines 273 and 276 inside the `if cfg.TemplatePath != ""` block were
returning a bare `error` instead of `(nil, error)`. The enclosing
`collectCPConfigFiles` function returns `(map[string]string, error)`,
so both error returns were missing the `nil` first value — causing
compile failures:

    cp_provisioner.go:273: not enough return values
    cp_provisioner.go:276: not enough return values

Fix: prefix both with `nil, `.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 19:27:40 +00:00
core-devops d4b4ff03f8 Revert "fix(handlers): add IsSaaS() and DefaultTier() methods"
Block internal-flavored paths / Block forbidden paths (pull_request) Successful in 22s
CI / Detect changes (pull_request) Successful in 44s
E2E Staging SaaS (full lifecycle) / E2E Staging SaaS (pull_request) Has been skipped
E2E API Smoke Test / detect-changes (pull_request) Successful in 45s
E2E Staging Canvas (Playwright) / detect-changes (pull_request) Successful in 52s
E2E Staging SaaS (full lifecycle) / pr-validate (pull_request) Successful in 57s
Harness Replays / detect-changes (pull_request) Successful in 35s
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 31s
qa-review / approved (pull_request) Failing after 32s
Handlers Postgres Integration / detect-changes (pull_request) Successful in 1m26s
security-review / approved (pull_request) Failing after 27s
Runtime PR-Built Compatibility / detect-changes (pull_request) Successful in 1m20s
lint-required-no-paths / lint-required-no-paths (pull_request) Successful in 1m45s
sop-checklist / na-declarations (pull_request) awaiting /sop-n/a declaration for: qa-review, security-review
sop-checklist / all-items-acked (pull_request) Successful in 32s
sop-tier-check / tier-check (pull_request) Successful in 24s
E2E Staging External Runtime / E2E Staging External Runtime (pull_request) Successful in 5m25s
CI / Shellcheck (E2E scripts) (pull_request) Successful in 9s
CI / Python Lint & Test (pull_request) Successful in 10s
Harness Replays / Harness Replays (pull_request) Successful in 13s
Runtime PR-Built Compatibility / PR-built wheel + import smoke (pull_request) Successful in 18s
E2E API Smoke Test / E2E API Smoke Test (pull_request) Failing after 1m40s
CI / Platform (Go) (pull_request) Failing after 4m38s
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Failing after 6m9s
E2E Staging Canvas (Playwright) / Canvas tabs E2E (pull_request) Successful in 9m54s
gate-check-v3 / gate-check (pull_request) Failing after 13m11s
CI / Canvas (Next.js) (pull_request) Successful in 17m27s
CI / Canvas Deploy Reminder (pull_request) Has been skipped
CI / all-required (pull_request) Successful in 9s
These methods already exist in workspace_dispatchers.go (lines 63/72)
at the base of this branch (45fb96e47). Adding them again to
workspace.go creates duplicate method definitions on the same receiver,
which causes a linker error on merge to main.

The call sites in workspace.go (IsSaaS, DefaultTier) and templates.go
resolve correctly through workspace_dispatchers.go — no change needed.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 19:05:12 +00:00
core-be 350be07910 fix(handlers): add IsSaaS() and DefaultTier() methods to WorkspaceHandler
Block internal-flavored paths / Block forbidden paths (pull_request) Successful in 13s
CI / Detect changes (pull_request) Successful in 45s
E2E Staging SaaS (full lifecycle) / E2E Staging SaaS (pull_request) Has been skipped
E2E API Smoke Test / detect-changes (pull_request) Successful in 1m0s
E2E Staging Canvas (Playwright) / detect-changes (pull_request) Successful in 1m3s
Harness Replays / detect-changes (pull_request) Successful in 30s
E2E Staging SaaS (full lifecycle) / pr-validate (pull_request) Successful in 54s
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 19s
Handlers Postgres Integration / detect-changes (pull_request) Successful in 1m10s
Runtime PR-Built Compatibility / detect-changes (pull_request) Successful in 55s
qa-review / approved (pull_request) Failing after 19s
gate-check-v3 / gate-check (pull_request) Failing after 28s
security-review / approved (pull_request) Failing after 14s
lint-required-no-paths / lint-required-no-paths (pull_request) Successful in 1m24s
sop-tier-check / tier-check (pull_request) Successful in 17s
sop-checklist / all-items-acked (pull_request) Successful in 18s
CI / Shellcheck (E2E scripts) (pull_request) Successful in 6s
CI / Python Lint & Test (pull_request) Successful in 6s
E2E API Smoke Test / E2E API Smoke Test (pull_request) Failing after 1m22s
Harness Replays / Harness Replays (pull_request) Successful in 9s
E2E Staging External Runtime / E2E Staging External Runtime (pull_request) Successful in 5m34s
CI / Platform (Go) (pull_request) Failing after 4m13s
Runtime PR-Built Compatibility / PR-built wheel + import smoke (pull_request) Successful in 10s
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Failing after 4m36s
E2E Staging Canvas (Playwright) / Canvas tabs E2E (pull_request) Successful in 8m52s
CI / Canvas (Next.js) (pull_request) Successful in 18m41s
CI / Canvas Deploy Reminder (pull_request) Has been skipped
CI / all-required (pull_request) Successful in 4s
PR #1051 inherited the IsSaaS()/DefaultTier() calls from PR #1047
(SaaS tier hard-gate) but the method definitions were missing after
the branch was force-updated to drop the errant 3f21d626 pre-compile
commit. Re-adding the definitions:

- IsSaaS() bool — returns h.cpProv != nil. True when the platform
  has a control-plane provisioner (SaaS tenant). False for self-hosted.

- DefaultTier() int — returns 3. Self-hosted default. SaaS workspaces
  always get Tier 4 via the IsSaaS() gate in Create regardless of
  what the client or template requests.

Fixes the CI / Platform (Go) compile failure.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 19:02:27 +00:00
core-devops 2bc33d579b fix(provisioner): skip symlinks in collectCPConfigFiles WalkDir (OFFSEC-010)
E2E Staging SaaS (full lifecycle) / E2E Staging SaaS (pull_request) Has been skipped
Block internal-flavored paths / Block forbidden paths (pull_request) Successful in 24s
Harness Replays / detect-changes (pull_request) Successful in 40s
E2E Staging SaaS (full lifecycle) / pr-validate (pull_request) Successful in 1m2s
E2E API Smoke Test / detect-changes (pull_request) Successful in 1m25s
CI / Detect changes (pull_request) Successful in 1m29s
E2E Staging Canvas (Playwright) / detect-changes (pull_request) Successful in 1m25s
Handlers Postgres Integration / detect-changes (pull_request) Successful in 1m15s
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 27s
Harness Replays / Harness Replays (pull_request) Successful in 9s
Runtime PR-Built Compatibility / detect-changes (pull_request) Successful in 36s
gate-check-v3 / gate-check (pull_request) Failing after 35s
qa-review / approved (pull_request) Failing after 26s
security-review / approved (pull_request) Failing after 24s
sop-tier-check / tier-check (pull_request) Successful in 20s
lint-required-no-paths / lint-required-no-paths (pull_request) Successful in 1m37s
CI / Python Lint & Test (pull_request) Successful in 7s
CI / Shellcheck (E2E scripts) (pull_request) Successful in 9s
E2E API Smoke Test / E2E API Smoke Test (pull_request) Failing after 1m3s
Runtime PR-Built Compatibility / PR-built wheel + import smoke (pull_request) Successful in 7s
CI / Platform (Go) (pull_request) Failing after 3m13s
E2E Staging External Runtime / E2E Staging External Runtime (pull_request) Successful in 5m30s
sop-checklist / na-declarations (pull_request) awaiting /sop-n/a declaration for: qa-review, security-review
sop-checklist / all-items-acked (pull_request) acked: 7/7 — body-unfilled: comprehensive-testing, local-postgres-e2e, staging-smoke, +4
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Failing after 4m7s
E2E Staging Canvas (Playwright) / Canvas tabs E2E (pull_request) Successful in 8m58s
CI / Canvas (Next.js) (pull_request) Successful in 15m30s
CI / Canvas Deploy Reminder (pull_request) Has been skipped
CI / all-required (pull_request) Successful in 14s
OFFSEC-010 (LOW): filepath.WalkDir follows symlinks by default. A
symlink inside a template configs dir pointing to /etc/passwd would
be traversed and included in config_files even though the subsequent
relative-path check correctly rejects it.

Two fixes:
1. Skip symlinks in the WalkDir callback (d.Type()&os.ModeSymlink guard).
   Defense-in-depth — WalkDir is told not to descend symlinks.
2. Reject cfg.TemplatePath if it is itself a symlink, preventing
   WalkDir from following it to an arbitrary directory and bypassing
   the cfg.TemplatePath boundary.

Added two tests:
- TestCollectCPConfigFiles_SkipsSymlinks: verifies symlinks inside
  template dir are not traversed.
- TestCollectCPConfigFiles_RejectsRootSymlink: verifies template path
  itself cannot be a symlink.

Refs: issue #1049 (OFFSEC-010)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 17:51:42 +00:00
2 changed files with 83 additions and 1 deletions
@@ -265,10 +265,28 @@ func collectCPConfigFiles(cfg WorkspaceConfig) (map[string]string, error) {
}
if cfg.TemplatePath != "" {
err := filepath.WalkDir(cfg.TemplatePath, func(path string, d os.DirEntry, walkErr error) error {
// Reject symlinks on the root itself — WalkDir follows symlinks,
// so a symlink TemplatePath that escapes the intended root directory
// would bypass the subsequent path-relativization checks below.
rootInfo, err := os.Lstat(cfg.TemplatePath)
if err != nil {
return nil, fmt.Errorf("collectCPConfigFiles: lstat template path: %w", err)
}
if rootInfo.Mode()&os.ModeSymlink != 0 {
return nil, fmt.Errorf("collectCPConfigFiles: template path must not be a symlink")
}
err = filepath.WalkDir(cfg.TemplatePath, func(path string, d os.DirEntry, walkErr error) error {
if walkErr != nil {
return walkErr
}
// Skip symlinks — WalkDir follows them by default, which means
// a symlink inside the template dir pointing to /etc/passwd
// would be traversed even though the resulting relative-path
// check would correctly reject it. Defense-in-depth: don't
// follow symlinks at all. (OFFSEC-010)
if d.Type()&os.ModeSymlink != 0 {
return nil
}
if d.IsDir() {
return nil
}
@@ -892,3 +892,67 @@ func TestIsRunning_EmptyInstanceIDReturnsFalse(t *testing.T) {
t.Errorf("IsRunning with empty instance_id should return running=false, got true")
}
}
// TestCollectCPConfigFiles_SkipsSymlinks — WalkDir follows symlinks by default,
// but collectCPConfigFiles must skip them so a symlink inside a template dir
// pointing outside (e.g. ln -s /etc snapshot) cannot be traversed.
// Verifies OFFSEC-010 defense-in-depth fix. (OFFSEC-010)
func TestCollectCPConfigFiles_SkipsSymlinks(t *testing.T) {
tmpl := t.TempDir()
// Write a real file that should be included.
if err := os.WriteFile(filepath.Join(tmpl, "config.yaml"), []byte("name: real\n"), 0o600); err != nil {
t.Fatal(err)
}
// Create a subdir with a file that will be symlinked-outside.
sensitiveDir := t.TempDir()
if err := os.WriteFile(filepath.Join(sensitiveDir, "secret.txt"), []byte("SENSITIVE\n"), 0o600); err != nil {
t.Fatal(err)
}
// Symlink inside template dir pointing to outside path.
symlinkPath := filepath.Join(tmpl, "snapshot")
if err := os.Symlink(sensitiveDir, symlinkPath); err != nil {
t.Fatal(err)
}
files, err := collectCPConfigFiles(WorkspaceConfig{TemplatePath: tmpl})
if err != nil {
t.Fatalf("collectCPConfigFiles: %v", err)
}
if files == nil {
t.Fatal("files should not be nil")
}
// config.yaml must be present.
if _, ok := files["config.yaml"]; !ok {
t.Errorf("config.yaml missing from files")
}
// The symlinked path must NOT be included (even though WalkDir would
// traverse it, the d.Type()&os.ModeSymlink guard skips the entry).
for k := range files {
if strings.Contains(k, "snapshot") || strings.Contains(k, "secret") {
t.Errorf("symlink path %q should not be in files — OFFSEC-010 regression", k)
}
}
}
// TestCollectCPConfigFiles_RejectsRootSymlink — if cfg.TemplatePath itself is
// a symlink, WalkDir would follow it to an arbitrary directory, bypassing the
// cfg.TemplatePath boundary. The function must reject this case explicitly.
// (OFFSEC-010)
func TestCollectCPConfigFiles_RejectsRootSymlink(t *testing.T) {
real := t.TempDir()
if err := os.WriteFile(filepath.Join(real, "config.yaml"), []byte("name: real\n"), 0o600); err != nil {
t.Fatal(err)
}
link := filepath.Join(t.TempDir(), "template-link")
if err := os.Symlink(real, link); err != nil {
t.Fatal(err)
}
_, err := collectCPConfigFiles(WorkspaceConfig{TemplatePath: link})
if err == nil {
t.Error("collectCPConfigFiles with symlink TemplatePath should return error")
}
if err != nil && !strings.Contains(err.Error(), "symlink") {
t.Errorf("expected symlink-related error, got: %v", err)
}
}