Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
20 changes: 18 additions & 2 deletions docs/reference-server.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Veer's first executable control-plane slice is the loopback-only
`veer-reference-server`. It is a deterministic contract and authorization
harness for issues #21 and #24, not the production `veer-api` service.
harness for issues #21, #24, and #26, not the production `veer-api` service.

## Proven boundary

Expand All @@ -19,7 +19,9 @@ and RESTRICT delete semantics for all six resource kinds:
Service tests cover valid and invalid hierarchy placement, generation and
resource-version changes, stale writes, keyed replays, idempotency conflicts,
concurrent writes, exact label filtering, stable `(createdAt, id)` ordering,
and opaque authenticated keyset pagination. The HTTP adapter exposes only the
opaque authenticated keyset pagination, and cross-Workspace denial. Every
store transaction is bound to one validated Workspace scope, while reads
require an explicit non-empty scope set. The HTTP adapter exposes only the
four paths and seven operations already published in
[`veer-v1alpha1.json`](../api/openapi/veer-v1alpha1.json). Child-resource route
topology remains intentionally unselected.
Expand All @@ -41,6 +43,15 @@ retained list row before it can influence page size or a cursor. Workspace
create and status replacement remain reserved and return `403` without a
resource or Operation mutation.

The in-memory adapter partitions resources and Operations by stable Workspace
ID. Missing scope is rejected before a callback runs, canonical ownership is
rechecked after decoding, and a root-list cursor is bound to the canonical set
of configured Workspace scopes. The global Operation route remains compatible
by performing bounded, individually scoped lookups over those configured
Workspaces; it never receives an unscoped store view. See the
[workspace isolation matrix](security/workspace-isolation.md) for the complete
evidence and deferred production boundaries.

Accepted mutations retain a bounded process-local admission record. Plan
construction replaces caller-supplied actor, decision, and Operation fields
with that record and current retained Operation. Immediately before a
Expand Down Expand Up @@ -81,6 +92,7 @@ Run the focused evidence set with:
```sh
go test ./internal/core/service/reference \
./internal/core/service/referenceauthorization \
./internal/core/domain/isolation \
./internal/adapters/store/memory \
./internal/adapters/referenceaccess \
./internal/transport/http \
Expand All @@ -89,6 +101,7 @@ go test ./internal/core/service/reference \

go test -race ./internal/core/service/reference \
./internal/core/service/referenceauthorization \
./internal/core/domain/isolation \
./internal/adapters/store/memory \
./internal/adapters/referenceaccess \
./internal/transport/http \
Expand Down Expand Up @@ -126,6 +139,9 @@ admission/revocation races, and effect/revocation exclusion.
that an external provider call can be cancelled after dispatch.
- There is no provider execution, queue, worker, audit sink, telemetry export,
persistent secret, cloud resource, or paid API call.
- Workspace partitioning is reference evidence only. Issue #30 owns composite
SQL ownership constraints, scoped query builders, runtime/database role
separation, forced RLS, migrations, and atomic state/audit/outbox commits.

The local cost is bounded to one process's CPU and memory plus loopback I/O.
The copy-on-write store favors deterministic reviewability over performance;
Expand Down
60 changes: 60 additions & 0 deletions docs/security/workspace-isolation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Workspace isolation evidence

Veer's process-local reference runtime treats production Workspaces as
mutually distrustful. Every tenant-owned read or write carries a stable opaque
Workspace ID; mutable display names are never accepted by the isolation,
storage, authorization, plan, audit, queue/worker, or provider-binding
contracts.

This document is the issue #26 isolation matrix. It records executable contract
evidence, not a claim that the deferred PostgreSQL, distributed worker, or
provider adapters already exist.

## Invariants

1. A missing or malformed scope fails before a store callback can observe or
mutate state.
2. A mutation callback is bound to exactly one Workspace partition.
3. A read callback sees only an explicit, non-empty canonical scope set.
4. Canonical resources and Operations must carry the same Workspace ID as the
partition from which they were loaded.
5. Root listing can cross Workspaces only through the runtime's explicit set of
configured member directories. Child listing accepts exactly one scope.
6. Page tokens bind the canonical scope-set digest; a token cannot be replayed
after widening or narrowing scope.
7. Queue data, Plans, audit references, and provider credential requests carry
stable IDs and reject mismatched bindings. None of those artifacts grants
execution authority.

## Isolation matrix

| Surface | Stable scope boundary | Negative evidence | Deferred production proof |
| --- | --- | --- | --- |
| Scope construction | `isolation.WorkspaceScope` and `WorkspaceScopeSet`; zero values are invalid | `TestWorkspaceScopeRejectsMissingAndMalformedIDs`; `TestWorkspaceScopeSetIsCanonicalAndOwnershipSafe` | None; this is the core value contract |
| Reference storage | `ReferenceStore.Update` requires one scope; `View` requires a non-empty scope set; memory state is partitioned by Workspace | `TestWorkspaceScopesFailClosedAndCannotCrossRead` proves callbacks are not invoked without scope, same resource IDs remain isolated, and a multi-scope identity collision fails closed | Composite foreign keys, query builders, runtime/database roles, forced RLS, and migration checks in issue #30 |
| CRUD and Operation reads | Create derives the root scope from the issued Workspace ID; addressed mutations and point reads require an explicit Workspace ID; loaded canonical ownership is rechecked | `TestWorkspaceScopeIsMandatoryAcrossCRUDListsAndOperations` rejects cross-scope get, replace, status replace, delete, Operation get, and foreign-parent create without changing the target | Durable transactions and tombstones in issue #30 |
| Lists and cursors | Workspace roots require an explicit scope set; child kinds require one scope; cursors bind a canonical scope digest | `TestWorkspaceScopeIsMandatoryAcrossCRUDListsAndOperations` rejects missing/duplicate scope and narrowed-scope token replay; `TestListFiltersUnauthorizedRowsBeforePagination` excludes an unconfigured Workspace before page shaping | Indexed durable pagination and query plans in issue #30 |
| Authorization | Policy targets resolve from stable hierarchy IDs and exact member directories; there is no display-name lookup | `TestEvaluateDefaultDenyScopeAndOrthogonalRoles`; `TestWorkspaceScopeRejectsMissingAndMalformedIDs`; the CRUD isolation test uses identical display names in two Workspaces | Durable membership and policy storage in issues #30 and #87 |
| Plans | `Plan` copies Workspace, resource, generation, Operation, decision, and provider bindings into its validated digest | `TestSameGenerationReplanRequiresDefinitiveAttemptsAndExactCarryForward` rejects prior attempts from another Plan/Workspace; `TestPlanBindsAdmissionAndExecutionRejectsPolicyDriftAndRevocation` reloads current scoped state | Executable step construction in issue #33 |
| Queue and worker authority | Work, lease, effect, attempt, and dispatch values are derived from one validated Plan and carry or digest its stable lineage | `TestDeliveryRequiresWorkAndLeaseFromTheSamePlan`; `TestAttemptPreparationDispatchAndOwnerLossAreFailClosed`; `TestDuplicateDeliveryNeverCreatesSecondAuthority` | Durable outbox/queue/worker implementations in issues #30, #31, and #32 |
| Audit | Workspace streams and every referenced target/Operation/elevation must agree on the stable Workspace ID | `TestProviderAttemptRequiresMatchingWorkspaceStream`; `TestOperationRequiresMatchingWorkspaceStream`; `TestReferenceStreamScopeIsExact`; `TestElevationCoReferencesMustMatchEverySharedScopeID` | Durable append-only sink and export controls in issues #30 and #88 |
| Provider adapter inputs | Credential requests seal Workspace, Environment, connection, target, Operation, action, and recipient bindings | `TestNewRequestRejectsUnsealedOrMismatchedInputs`; `TestValidateRequestRejectsEveryForgedBindingDimension`; `TestEveryBindingOnlyDimensionChangesOnlyBindingDigest` | Real broker/provider integration in issues #38, #39, #45, #52, and #87 |

The named tests live under `internal/` and run in the aggregate repository
checks. The reference HTTP contract is unchanged: child-resource routes remain
unselected, and the global Operation route performs bounded lookup by querying
each configured Workspace scope separately before authorization. No unscoped
storage view is exposed to that route.

## Residual risk and cost

The in-memory adapter holds one process-wide lock and copies one Workspace
partition per mutation. Explicit multi-Workspace root lists build a bounded
union under the same lock. This favors deterministic isolation evidence over
throughput; it does not establish production latency, memory, or database cost.

Issue #30 must preserve these fail-closed interfaces with parameterized scoped
queries, composite ownership constraints, a non-owner runtime role, forced RLS
as defense in depth, and atomic state/audit/outbox commits. Issues #31, #32,
and #33 must preserve the same stable scope through delivery and execution. Real
provider adapters remain blocked on their own dependency-ready issues.
75 changes: 62 additions & 13 deletions internal/adapters/store/memory/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,60 +9,93 @@ import (
"errors"
"sync"

"github.com/ArdurAI/veer/internal/core/domain/isolation"
"github.com/ArdurAI/veer/internal/core/domain/resource"
"github.com/ArdurAI/veer/internal/core/ports"
)

var ErrInvalidCallback = errors.New("invalid reference-store callback")
var (
ErrInvalidCallback = errors.New("invalid reference-store callback")
ErrIdentityCollision = errors.New("reference-store identity exists in multiple Workspace scopes")
)

// Store is a serializable, copy-on-write reference store. Copy-on-write makes
// callback failure observably atomic without leaking a mutable map alias.
type Store struct {
mu sync.RWMutex
resources map[resource.ID][]byte
operations map[resource.ID][]byte
resources map[resource.ID]map[resource.ID][]byte
operations map[resource.ID]map[resource.ID][]byte
}

// NewStore creates an empty initialized reference store.
func NewStore() *Store {
return &Store{
resources: make(map[resource.ID][]byte),
operations: make(map[resource.ID][]byte),
resources: make(map[resource.ID]map[resource.ID][]byte),
operations: make(map[resource.ID]map[resource.ID][]byte),
}
}

// View runs callback against one locked, consistent state view.
func (store *Store) View(ctx context.Context, callback func(ports.ReferenceReader) error) error {
// View runs callback against one locked view containing only explicit scopes.
func (store *Store) View(
ctx context.Context,
scopes isolation.WorkspaceScopeSet,
callback func(ports.ReferenceReader) error,
) error {
if store == nil || callback == nil {
return ErrInvalidCallback
}
if err := isolation.ValidateWorkspaceScopeSet(scopes); err != nil {
return err
}
if ctx == nil {
return ErrInvalidCallback
}
if err := ctx.Err(); err != nil {
return err
}
store.mu.RLock()
defer store.mu.RUnlock()
if err := callback(reader{resources: store.resources, operations: store.operations}); err != nil {
resources, err := scopedView(store.resources, scopes)
if err != nil {
return err
}
operations, err := scopedView(store.operations, scopes)
if err != nil {
return err
}
if err := callback(reader{resources: resources, operations: operations}); err != nil {
return err
}
return ctx.Err()
}

// Update commits the callback's private working copy only after the callback
// and its context both succeed.
func (store *Store) Update(ctx context.Context, callback func(ports.ReferenceTransaction) error) error {
func (store *Store) Update(
ctx context.Context,
scope isolation.WorkspaceScope,
callback func(ports.ReferenceTransaction) error,
) error {
if store == nil || callback == nil {
return ErrInvalidCallback
}
if err := isolation.ValidateWorkspaceScope(scope); err != nil {
return err
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
}
if ctx == nil {
return ErrInvalidCallback
}
if err := ctx.Err(); err != nil {
return err
}
store.mu.Lock()
defer store.mu.Unlock()

workspaceID := scope.WorkspaceID()
working := &transaction{
reader: reader{
resources: cloneMap(store.resources),
operations: cloneMap(store.operations),
resources: cloneMap(store.resources[workspaceID]),
operations: cloneMap(store.operations[workspaceID]),
},
}
if err := callback(working); err != nil {
Expand All @@ -71,8 +104,8 @@ func (store *Store) Update(ctx context.Context, callback func(ports.ReferenceTra
if err := ctx.Err(); err != nil {
return err
}
store.resources = working.resources
store.operations = working.operations
store.resources[workspaceID] = working.resources
store.operations[workspaceID] = working.operations
return nil
}

Expand Down Expand Up @@ -121,4 +154,20 @@ func cloneMap(source map[resource.ID][]byte) map[resource.ID][]byte {
return result
}

func scopedView(
source map[resource.ID]map[resource.ID][]byte,
scopes isolation.WorkspaceScopeSet,
) (map[resource.ID][]byte, error) {
result := make(map[resource.ID][]byte)
for _, scope := range scopes.Scopes() {
for id, value := range source[scope.WorkspaceID()] {
if _, exists := result[id]; exists {
return nil, ErrIdentityCollision
}
result[id] = value
}
}
return result, nil
}

var _ ports.ReferenceStore = (*Store)(nil)
Loading