Skip to content
Draft
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Managed external resource reconciliation

Planning workspace for the durable reconciliation layer that follows the process-local Daytona
Secret delivery in PR #5277. This branch contains design documents only; it does not add API code,
migrations, workers, or deployment wiring.

## Files

- `context.md` - Problem, goals, non-goals, and success criteria.
- `research.md` - Existing Agenta patterns and candidate reuse cases.
- `design.md` - Proposed reusable managed-resource boundary.
- `api-design.md` - Reviewable API, data, identity, and migration shape.
- `plan.md` - Stack boundaries, design gate, sequencing, and acceptance tests.
- `status.md` - Current state, decisions, open questions, and next action.
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
# API design

This document is the proposed lower-level shape for PR B. Names remain reviewable until the CTO
approves the domain boundary.

## Domain placement

```text
api/oss/src/apis/fastapi/managed_resources/
api/oss/src/core/managed_resources/
api/oss/src/dbs/postgres/managed_resources/
```

Follow the standard Router -> Service -> DAO interface -> Postgres DAO dependency direction.

## Data model

### `managed_resource_sets`

One set represents a controller-owned realization of one product/runtime owner generation.

| Field | Role |
| --- | --- |
| `id` | Internal resource-set identity |
| `project_id` | Tenant scope |
| `owner_kind`, `owner_id` | Product or runtime owner reference |
| `controller_key` | Routes reconciliation to a typed controller |
| `idempotency_key` | Deduplicates reservation attempts |
| `specification_digest` | Detects owner-generation changes without storing secrets |
| `generation` | Monotonic desired generation |
| `desired_state` | `present` or `absent` |
| `observed_state` | `pending`, `present`, `absent`, `unknown`, or `blocked` |
| `version` | Optimistic compare-and-set version |
| `attempt_count`, `next_attempt_at` | Retry scheduling |
| `error_code` | Bounded non-secret error |
| `claim_id`, `claim_owner`, `claim_generation`, `claim_expires_at` | Fenced worker claim |
| timestamps | Creation, update, and terminal history |

Unique constraints:

- `(project_id, controller_key, idempotency_key)`;
- stable cursor index `(created_at, id)`;
- due-work index `(controller_key, desired_state, observed_state, next_attempt_at, created_at, id)`.

### `managed_resources`

One row represents one external child resource in a set.

| Field | Role |
| --- | --- |
| `id`, `resource_set_id` | Internal identity and parent |
| `logical_key` | Stable controller-owned key within the set |
| `provider_key`, `resource_kind` | External system and typed resource kind |
| `external_id`, `external_name` | Provider identity needed for observe/delete |
| `observed_state` | Child observation |
| `version` | Child compare-and-set version |
| `safe_attributes` | Optional versioned, controller-validated, non-secret recovery facts |
| timestamps | Lifecycle history |

`safe_attributes` must be a discriminated typed payload for registered resource kinds. It is not an
unrestricted metadata bucket. Daytona Secret rows should not store credential values, headers,
provider responses, or control tokens.

## Internal operations

The initial API surface should be workload-only:

```text
POST /internal/managed-resources/reserve
GET /internal/managed-resources/{id}
PATCH /internal/managed-resources/{id}/intent
POST /internal/managed-resources/query-due
POST /internal/managed-resources/{id}/claim
PATCH /internal/managed-resources/{id}/observation
```

Exact path naming is less important than keeping the surface internal and avoiding special cases in
global authentication middleware.

Every mutation includes `expected_version`. Observation writes from a worker also include the claim
ID and claim generation. A stale claim or version returns a conflict without changing state.

## Workload identity

PR B must choose one standard internal workload-auth mechanism. The current #5242 approach adds
exact path matching to shared auth middleware and relies on invocation authorization that was
temporarily carried through telemetry context. Do not preserve that coupling.

The preferred contract is:

```text
service-stamped run context {
tenant scope
run/session owner
workload authorization or signed assertion
}
```

The runner uses its workload identity for control-plane access and the signed context for tenant
scope. Provider credentials remain separate from both.

## Reconciler execution

1. Query due sets by `controller_key` and immutable cursor.
2. Atomically claim one set with a new claim generation.
3. Load the current owner generation or typed controller plan.
4. Observe the provider outside a database transaction.
5. Apply the smallest idempotent provider operation.
6. Record child and set observations using version and claim fences.
7. On a retryable failure, record a safe error and bounded backoff.
8. On an invariant violation or ambiguous ownership, mark `blocked` for operator review.

TaskIQ can wake or schedule controllers. Postgres remains the due-work authority so a lost queue
message cannot permanently lose reconciliation.

## Daytona mapping

| Current Daytona field | Proposed location |
| --- | --- |
| session/run owner | resource-set owner reference |
| credential epoch digest | resource-set specification digest |
| Secret provider ID/name | child external ID/name |
| sandbox ID | separate child resource or typed safe attribute |
| allowed host and binding | Daytona controller plan, not universal columns |
| cleanup retry | resource-set retry state |
| janitor claim | generic resource-set claim |

The Daytona controller may store a versioned safe attribute required to verify exact-host metadata.
That payload remains a typed Daytona shape rather than a column required by other controllers.

## Migration policy

Migration `oss000000011_add_agent_secret_leases.py` has not shipped. Replace it before merge. Do
not ship a Daytona schema followed immediately by a production rename migration.

Preview databases that already ran migration 011 should downgrade to 010 and apply the replacement
011. If a shared preview cannot be reset, use a one-off development repair rather than permanent
production compatibility code for unshipped data.
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Context

The original PR #5242 combined two decisions: deliver opaque HTTP credentials through exact-host
Daytona Secrets, and persist crash-safe lifecycle state for external resources. The first decision
does not require an API migration and now lives in PR #5277. The second does require a reviewed
control-plane design and is the subject of this plan-only stacked PR.

## Goals

1. Preserve PR #5277 as the functional Daytona isolation layer with no API changes.
2. Keep the lower implementation reusable when durability is added.
3. Design a small managed-external-resource kernel using existing Agenta conventions.
4. Keep product domains such as connections and triggers as their own sources of truth.
5. Keep plaintext credentials and arbitrary provider responses out of the generic control plane.

## Non-goals

- Building a generic workflow engine.
- Replacing gateway connections, trigger subscriptions, webhooks, or session ownership.
- Migrating Composio triggers in the first durability PR.
- Enabling the lower PR in production before orphan recovery exists.
- Implementing the durable API, schema, worker, or controller in this planning PR.

## Success criteria

- PR #5277 contains no API router, migration, auth-middleware change, or durable janitor.
- PR #5277 proves plaintext opaque credentials do not enter Daytona sandbox configuration.
- The approved implementation adds durability without changing PR #5277's credential plan or
Daytona provider adapter.
- The durable schema uses generic ownership and reconciliation concepts, not Daytona-only columns.
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
# Design

## Boundary

The shared abstraction is a managed external resource: something Agenta creates in another system,
which may survive the process that created it and must eventually converge to a requested state.

The kernel owns durable intent and coordination. Controllers own provider behavior.

```text
Product or runtime owner
-> managed-resource intent
-> durable store
-> controller claim
-> provider operation
-> observed-state compare-and-set
```

Product records remain the user-facing source of truth. A trigger subscription remains a trigger
subscription. A connection remains a gateway connection. Managed-resource records are internal
realization records linked to those owners.

## Common kernel

The common kernel owns:

- tenant scope;
- owner reference;
- controller key;
- idempotency key;
- specification digest and generation;
- desired state;
- observed state;
- external resource identity;
- optimistic version;
- attempt count and next attempt time;
- bounded safe error;
- claim identity, generation, and expiry;
- lifecycle timestamps.

It never stores plaintext credentials, arbitrary provider responses, user-facing connection
configuration, or provider control-plane credentials.

## Controller boundary

Each controller understands one resource kind and exposes a small port:

```text
plan(owner, generation) -> typed provider plan
observe(record) -> provider observation
apply(plan, observation) -> provider result
destroy(record) -> provider result
```

The first controller is `daytona.secret_bundle.v1`. It remains in the runner because the runner
already owns resolved model credentials, Daytona credentials, sandbox creation, and warm-session
lifecycle.

A later Composio trigger controller can live beside the triggers service and run in an API worker.
The kernel does not require every controller to run in the same process.

## Generic state

Prefer desired and observed state over a universal transition enum.

```text
desired: present | absent
observed: pending | present | absent | unknown | blocked
```

Provider-specific controllers validate their own transition rules. The shared store only enforces
claim fencing, optimistic versions, valid common states, and safe retry scheduling.

This avoids pretending that Daytona Secret provisioning, OAuth connection activation, and trigger
subscription enablement share one detailed state machine.

## PR A process-local seam

PR A defines the Daytona credential plan and provider adapter independently from durable storage.
It uses a process-local allocation record attached to the existing session environment:

```text
DaytonaSecretAllocation {
sandboxId
credentialEpochDigest
resources: [{logicalKey, externalId, externalName}]
}
```

Normal teardown and failed sandbox creation call the same provider cleanup functions PR B will use.
A hard process crash may still orphan resources. The feature stays disabled for production until
PR B removes that limitation.

## PR B durable seam

PR B adds a `ManagedResourcesControl` port. The runner uses an HTTP implementation backed by the
API domain. Tests use an in-memory implementation. The Daytona plan and provider adapter do not
change.

```text
ManagedResourcesControl {
reserve(intent)
retrieve(id)
setDesiredState(id, state, expectedVersion)
recordObserved(id, result, expectedVersion, claim)
queryDue(controllerKey, cursor, limit)
claim(id, owner, ttl)
}
```

The internal API should use one workload-auth dependency or internal router. It should not add
feature-specific path recognition to global authentication middleware. Tenant scope must come from
a typed service-stamped run context or other approved workload identity, not from telemetry fields.

## Reuse cases

1. Daytona Secret and sandbox cleanup.
2. Composio trigger instances and project webhook registrations.
3. Provider-side gateway connection initiation and revocation.
4. Future MCP OAuth registrations or managed gateway allocations.
5. Remote relay endpoints, tunnels, or preview sandboxes if they become persistent provider
resources.

Only Daytona ships in PR B. The trigger controller is the next recommended adoption and the proof
that the kernel is reusable.

## Guardrails

- Do not create an arbitrary JSON workflow engine.
- Do not put credentials in `attributes` or `metadata`.
- Do not merge product DTOs with realization DTOs.
- Do not use age alone to decide that a resource is orphaned.
- Do not hold a database transaction while calling a provider.
- Claim work, call the provider outside the transaction, then compare-and-set observed state.
- Every provider create/delete operation must be idempotent or reconciled through observe-before-act.
Loading
Loading