fix(security): harden upstream critical surfaces (#32)

* fix(security): harden upstream critical surfaces

Refs #30

* fix(security): close pre-landing review gaps

Refs #30

* fix(security): close official release blockers
This commit is contained in:
Duy /zuey/ authored and GitHub committed 2026-05-20 16:33:49 +07:00
1 parent 2a48e62c6b
commit 532ff91d8e
59 files changed
+2194 -397

No files matched your search

+8 -1
View File
@@ -16,7 +16,7 @@ var schemaSQL string
// SchemaVersion is the current SQLite schema version.
// Bump this when adding new migration steps below.
const SchemaVersion = 36
const SchemaVersion = 37
// migrations maps version → SQL to apply when upgrading FROM that version.
// schema.sql always represents the LATEST full schema (for fresh DBs).
@@ -559,6 +559,8 @@ CREATE TABLE IF NOT EXISTS agent_workstation_links (
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
PRIMARY KEY (agent_id, workstation_id)
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_agent_workstation_default
ON agent_workstation_links(agent_id) WHERE is_default = 1;
CREATE INDEX IF NOT EXISTS idx_agent_workstation_tenant ON agent_workstation_links(tenant_id);`,
// Version 31 → 32: workstation_permissions allowlist table. Mirrors PG migration 000063.
@@ -610,6 +612,11 @@ WHERE id IN (
OR (s.is_system = 0 AND sag.tenant_id <> s.tenant_id)
);`,
// Version 36 → 37: enforce one default workstation link per agent.
// Mirrors PG migration 000062 partial unique index.
36: `CREATE UNIQUE INDEX IF NOT EXISTS idx_agent_workstation_default
ON agent_workstation_links(agent_id) WHERE is_default = 1;`,
// Version 23 → 24: vault_documents scope/ownership consistency triggers.
// Mirrors PG migration 000055 CHECK constraint; SQLite cannot add CHECK via
// ALTER TABLE so we use BEFORE INSERT + BEFORE UPDATE triggers instead.
+2
View File
@@ -1774,6 +1774,8 @@ CREATE TABLE IF NOT EXISTS agent_workstation_links (
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
PRIMARY KEY (agent_id, workstation_id)
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_agent_workstation_default
ON agent_workstation_links(agent_id) WHERE is_default = 1;
CREATE INDEX IF NOT EXISTS idx_agent_workstation_tenant ON agent_workstation_links(tenant_id);
-- ============================================================
@@ -48,6 +48,42 @@ func TestEnsureSchema_FreshDB(t *testing.T) {
t.Errorf("vault_documents missing column %q", want)
}
}
for _, table := range []string{"hooks", "hook_agents"} {
var count int
if err := db.QueryRow(`SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name=?`, table).Scan(&count); err != nil {
t.Fatalf("lookup %s table: %v", table, err)
}
if count != 1 {
t.Errorf("fresh schema missing %q table", table)
}
}
}
func TestEnsureSchema_PreHooksUpgradeCreatesHookTables(t *testing.T) {
db := openTestDBAtVersion(t, 19)
for _, table := range []string{"tenant_hook_budget", "hook_executions", "hook_agents", "hooks"} {
if _, err := db.Exec(`DROP TABLE IF EXISTS ` + table); err != nil {
t.Fatalf("drop %s: %v", table, err)
}
}
if _, err := db.Exec(`UPDATE schema_version SET version = 19`); err != nil {
t.Fatalf("set pre-hooks schema version: %v", err)
}
if err := EnsureSchema(db); err != nil {
t.Fatalf("EnsureSchema (pre-hooks to current) failed: %v", err)
}
for _, table := range []string{"hooks", "hook_agents"} {
var count int
if err := db.QueryRow(`SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name=?`, table).Scan(&count); err != nil {
t.Fatalf("lookup %s table: %v", table, err)
}
if count != 1 {
t.Errorf("upgrade schema missing %q table", table)
}
}
}
// TestEnsureSchema_MigrationV11Only verifies migrations from v11 onward
+7 -4
View File
@@ -70,11 +70,13 @@ func (s *SQLiteWebhookCallStore) Create(ctx context.Context, call *store.Webhook
`INSERT INTO webhook_calls
(id, tenant_id, webhook_id, agent_id, delivery_id,
idempotency_key, mode, status, callback_url, attempts,
next_attempt_at, request_payload, created_at)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)`,
next_attempt_at, started_at, request_payload, response, last_error,
created_at, completed_at)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`,
call.ID, call.TenantID, call.WebhookID, nilUUID(call.AgentID), call.DeliveryID,
call.IdempotencyKey, call.Mode, call.Status, call.CallbackURL, call.Attempts,
call.NextAttemptAt, call.RequestPayload, call.CreatedAt,
call.NextAttemptAt, call.StartedAt, call.RequestPayload, call.Response, call.LastError,
call.CreatedAt, call.CompletedAt,
)
if err != nil {
// Map partial unique index violation (webhook_id, idempotency_key) → typed sentinel.
@@ -154,6 +156,7 @@ func (s *SQLiteWebhookCallStore) ClaimNext(ctx context.Context, tenantID uuid.UU
err = tx.QueryRowContext(ctx,
`SELECT id FROM webhook_calls
WHERE tenant_id = ?
AND mode = 'async'
AND status = 'queued'
AND (next_attempt_at IS NULL OR next_attempt_at <= ?)
ORDER BY next_attempt_at ASC
@@ -265,7 +268,7 @@ func (s *SQLiteWebhookCallStore) ReclaimStale(ctx context.Context, staleThreshol
res, err := s.db.ExecContext(ctx,
`UPDATE webhook_calls
SET status = 'queued', started_at = NULL, lease_token = NULL
WHERE status = 'running' AND started_at < ?`,
WHERE mode = 'async' AND status = 'running' AND started_at < ?`,
staleThreshold,
)
if err != nil {
@@ -160,6 +160,25 @@ func TestWebhookCallClaimNextSkipsRunningAndDone(t *testing.T) {
t.Errorf("expected ErrNoRows when no queued rows, got: %v", err)
}
// A queued sync audit row is not worker-owned and must not be claimed.
syncQueued := &store.WebhookCallData{
ID: uuid.New(),
TenantID: tenantID,
WebhookID: wh.ID,
DeliveryID: uuid.New(),
Mode: "sync",
Status: "queued",
Attempts: 0,
CreatedAt: now,
}
if err := cs.Create(ctx, syncQueued); err != nil {
t.Fatalf("Create queued sync call: %v", err)
}
_, err = cs.ClaimNext(ctx, tenantID, now)
if err != sql.ErrNoRows {
t.Errorf("expected ErrNoRows for queued sync row, got: %v", err)
}
// Insert a queued call due now.
queued := &store.WebhookCallData{
ID: uuid.New(),
@@ -194,6 +213,65 @@ func TestWebhookCallClaimNextSkipsRunningAndDone(t *testing.T) {
}
}
func TestWebhookCallReclaimStaleOnlyAsync(t *testing.T) {
db := openTestWebhookDB(t)
ws := NewSQLiteWebhookStore(db)
cs := NewSQLiteWebhookCallStore(db)
tenantID := uuid.New()
ctx := testTenantCtx(tenantID)
wh := &store.WebhookData{
ID: uuid.New(), TenantID: tenantID, Name: "wh-reclaim", Kind: "llm",
SecretHash: "h-reclaim", Scopes: []string{}, IPAllowlist: []string{},
RateLimitPerMin: 60, CreatedAt: time.Now().UTC(), UpdatedAt: time.Now().UTC(),
}
if err := ws.Create(ctx, wh); err != nil {
t.Fatalf("Create webhook: %v", err)
}
stale := time.Now().UTC().Add(-time.Hour)
rows := []struct {
mode string
id uuid.UUID
}{
{mode: "sync", id: uuid.New()},
{mode: "async", id: uuid.New()},
}
for _, row := range rows {
_, err := db.ExecContext(ctx,
`INSERT INTO webhook_calls (id,tenant_id,webhook_id,delivery_id,mode,status,attempts,created_at,started_at)
VALUES (?,?,?,?,?,?,?,?,?)`,
row.id, tenantID, wh.ID, uuid.New(), row.mode, "running", 0, stale, stale,
)
if err != nil {
t.Fatalf("insert %s row: %v", row.mode, err)
}
}
n, err := cs.ReclaimStale(ctx, time.Now().UTC())
if err != nil {
t.Fatalf("ReclaimStale: %v", err)
}
if n != 1 {
t.Fatalf("reclaimed %d rows, want 1", n)
}
var syncStatus string
if err := db.QueryRowContext(ctx, `SELECT status FROM webhook_calls WHERE id = ?`, rows[0].id).Scan(&syncStatus); err != nil {
t.Fatalf("select sync row: %v", err)
}
if syncStatus != "running" {
t.Fatalf("sync row status = %q, want running", syncStatus)
}
var asyncStatus string
if err := db.QueryRowContext(ctx, `SELECT status FROM webhook_calls WHERE id = ?`, rows[1].id).Scan(&asyncStatus); err != nil {
t.Fatalf("select async row: %v", err)
}
if asyncStatus != "queued" {
t.Fatalf("async row status = %q, want queued", asyncStatus)
}
}
// TestWebhookCallIdempotencyConflict verifies duplicate (webhook_id, idempotency_key)
// returns ErrIdempotencyConflict.
func TestWebhookCallIdempotencyConflict(t *testing.T) {