diff --git a/docs/Design.md b/docs/Design.md index 3d097e29..711d1ff2 100644 --- a/docs/Design.md +++ b/docs/Design.md @@ -8,6 +8,34 @@ Derrick does not provide many tools to agents. Instead Derrick provides a small ## Structure This application is Protocol first. All major features must have a Protocol and internally use GoF Design Patterns. No exceptions. The Protocols can be found in the Structure spm. +## Core redesign vocabulary + +The core redesign uses these terms consistently: + +- **Module** — functionality running inside an existing process. +- **Service** — a standalone process with its own lifecycle and IPC boundary. +- **Actor** — anything attempting a capability, including an operator, UI, agent, plugin guest, module, or service. +- **SideEffect** — a host-mediated operation with an external consequence, such as network access, UI presentation, container execution, persistence mutation, or message delivery. +- **Command** — a typed request asking an endpoint to perform work. +- **Event** — a typed notification that something happened; it does not expect a response. +- **Model message** — model conversation data such as system, user, assistant, and tool messages. +- **Service message** — internal module/service communication. +- **Plugin SideEffect message** — a guest-to-host request such as `http.request` or `ui.present`. +- Concrete in-process implementations end in `Module`; standalone process implementations end in `Service`. +- Behavioral protocols use suffixes such as `Serving`, `Checking`, `Evaluating`, `Applying`, `Executing`, and `Storing`. + +The core boundary is: + +1. Validate the message contract. +2. Identify the Actor. +3. Check the Actor's capabilities. +4. Evaluate Guardrail Policy. +5. Apply the decision. +6. Execute the approved SideEffect. +7. Return a typed result and audit event. + +The kernel authority owns lifecycle, capabilities, secrets, SideEffects, and Guardrail enforcement. Modules provide replaceable in-process functionality. Services provide standalone process boundaries. Plugins remain user-space programs and never authorize their own SideEffects. + ## Guardrail Guardrail is Derrick's control plane in Structure (`Sources/Guardrail`). diff --git a/docs/RedesignPlan.md b/docs/RedesignPlan.md new file mode 100644 index 00000000..55292f09 --- /dev/null +++ b/docs/RedesignPlan.md @@ -0,0 +1,393 @@ +# Derrick Core Redesign Plan + +## Scope + +This plan covers item 1 of issue 47: the core redesign. The Chat, Plugin, and +Configuration screen redesign is item 2 and is deferred until this plan is +complete. + +The design is microkernel-inspired: + +- The kernel owns authority, lifecycle, boundaries, secrets, and SideEffects. +- Modules provide replaceable in-process functionality. +- Services provide standalone process boundaries. +- Plugins are user-space programs. + +## Naming + +Concrete names must identify their execution boundary: + +- `AgentOrchestrationModule` — in-process implementation. +- `JobSchedulingModule` — in-process implementation. +- `SessionMemoryModule` — in-process implementation. +- `NetworkSideEffectModule` — in-process implementation. +- `UISideEffectModule` — in-process implementation. +- `MCPClientModule` and `MCPServerModule` — in-process implementations. +- `AgentService`, `JobService`, `MCPService`, and `DockerRunnerService` — + standalone processes. + +Behavioral protocols retain behavioral suffixes: + +- `*Serving` +- `*Checking` +- `*Evaluating` +- `*Applying` +- `*Executing` +- `*Storing` + +Names must not become `ModuleModule` or `ServiceService`. + +## Protocol rule + +No existing protocol may be changed and no new protocol may be added without +first presenting the exact protocol and types to the operator for approval. + +No module or feature may be built without an approved protocol contract and +typed request, response, and event types. + +Existing `Structure` protocols are the starting point. The first implementation +step after Phase 0 is a protocol decision gate. + +## Phase 0 — Baseline and architecture audit + +Status: complete. + +Completed: + +- Created `refactor/core-redesign` from merged `main`. +- Audited package dependencies and direct implementation imports. +- Audited AgentRuntime orchestration. +- Audited the current scheduler and daemon bootstrap. +- Locked terminology in `docs/Design.md`. +- Confirmed no protocol was changed or added. + +### Scheduler finding + +`JobServiceScheduler` already exists. It polls schedules and jobs, claims work, +recovers interrupted jobs, and invokes job execution. + +`DaemonModuleBootstrap` currently starts it in-process inside `derrickd` and +marks the jobs module ready. This plan does **not** move a scheduler from a +standalone service to a module. The current runtime is already in-process; the +directory name does not establish a process boundary. + +The first implementation will formalize the existing behavior as a +`JobSchedulingModule`. A future standalone implementation may be named +`JobSchedulingService`, but that process extraction is not part of the first +refactor. + +## Phase 1 — Protocol decision gate + +Status: approved and implemented as `Structure` contracts. Runtime adapters and +caller migration are deferred to later phases. + +Implemented in this phase: + +- `AgentRegistryManaging` +- `AgentDirecting` +- `AgentOrchestrationServing` +- `ActorID`, `ActorKind`, and `AgentKind` +- Capability request, decision, set, and checking contracts +- Process supervision request, handle, status, and checking contract +- `JobSchedulingServing` +- Host, network, and UI SideEffect execution contracts +- Host secret scope, reference, and attachment contracts +- `AgentConfigurationReference` + +No host module or service implementation was added in this phase. + +### Agent orchestration + +Use these existing protocols as the base: + +- `AgentMailboxing` +- `AgentRegistryManaging` — authoritative live `AgentInstance` registry +- `AgentDirecting` +- `TurnRunning` + +Propose a composite `AgentOrchestrationServing` contract only after reviewing +those existing shapes. `AgentRegistryManaging` owns the authoritative live +`AgentInstance` registry. `AgentDirecting` owns delegation, spawn, cancel, and +route decisions. `AgentMailboxing` owns queue operations. + +### Job scheduling + +Propose `JobSchedulingServing` for start, stop, health, claiming, recovery, and +cancellation. The first implementation wraps `JobServiceScheduler`; it does +not create a second scheduler. + +### Process supervision + +Propose `ProcessSupervising` for launch, stop, cancel, health, restart, +timeouts, resource limits, and orphan cleanup. + +### Actors and capabilities + +Propose typed contracts for: + +- `ActorID` +- `ActorKind` +- `AgentKind` +- `CapabilityID` +- `CapabilitySet` +- `CapabilityRequest` +- Capability decisions + +`agentID` remains the identity of an agent and is not silently reinterpreted as +the universal Actor identity. + +`ActorKind` identifies who is attempting a capability, such as `operator`, +`ui`, `agent`, `pluginGuest`, `module`, `service`, `job`, `workflow`, or +`system`. + +`AgentKind` applies only to an agent and identifies how it exists, such as +`interactive`, `delegated`, `scheduled`, `workflow`, `integration`, or +`system`. It is separate from `AgentRole`, `AgentStatus`, and +`AgentConfiguration`. + +### SideEffect Broker + +Propose: + +- `HostSideEffectExecuting` +- `NetworkSideEffectExecuting` +- `UISideEffectExecuting` +- Host secret resolution and attachment contracts + +## Phase 2 — Separate message families + +Use separate typed families: + +1. **Service messages** — commands, requests, responses, lifecycle messages, + events, actor context, correlation, cancellation, and errors between host + modules and services. +2. **Model messages** — system, user, assistant, and tool conversation data, + model requests, streaming chunks, usage, and provider failures. +3. **Plugin SideEffect messages** — `http.request`, `ui.present`, tool requests, + result envelopes, and artifact requests. +4. **AppEvents** — host notifications, not RPC or authorization. + +`sourceService` and `destinationService` identify host endpoints. A model +provider is addressed through a model client/provider adapter, not as a +Derrick service. + +## Phase 3 — Kernel runtime and process supervision + +Create the composition boundary for modules, services, SideEffect providers, +Guardrail, capabilities, process supervision, and transport. + +The supervisor manages standalone services, Docker guests, plugin runtimes, and +helper processes. An in-process module is supervised as part of its containing +process. + +Define startup, shutdown, readiness, restart, backoff, timeout, cancellation, +resource limits, orphan cleanup, and audit behavior. + +Every launch follows: + +```text +Message validation +→ Actor identification +→ Capability check +→ Guardrail evaluation +→ Approved launch plan +→ Process supervisor +``` + +## Phase 4 — AgentOrchestrationModule consolidation + +Consolidate these existing pieces behind one approved module contract: + +- `AgentMailboxing` +- `AgentRegistryManaging` +- `AgentDirecting` +- `TurnRunning` +- `InMemoryAgentDirectory` +- `InMemoryMailbox` +- `HierarchicalOrchestrator` +- `SessionOrchestrator` +- Agent turn routing + +The `AgentOrchestrationModule` composes `AgentRegistryManaging`, +`AgentDirecting`, and `AgentMailboxing`. It owns registration, parent/child +relationships, mailboxes, turn scheduling, concurrency, depth limits, +cancellation, status, and result routing. + +It works with `SessionMemoryModule`, but they remain separate: + +- Orchestration owns live coordination. +- Session memory owns retrieval, ingestion, summaries, compaction, and archive. +- Inbox messages are not automatically memory records. + +The first implementation is in-process. A future `AgentOrchestrationService` +can implement the same contract without changing callers. + +### AgentConfiguration and instance identity + +`AgentProfile` terminology is replaced by `AgentConfiguration`: + +- `AgentConfiguration` — durable configuration/template. +- `AgentConfigurationCatalog` — persistence and lookup contract. +- `AgentConfigurationTurnContext` — resolved configuration for a turn. +- `configurationHandle` — human-facing configuration identifier. + +An LLM model remains a separate concept. An `AgentConfiguration` references +model settings, but it is not the model and it is not the runtime agent. + +The runtime identity model is: + +- `AgentInstance` — the live domain agent. +- `AgentRuntime` — in-memory execution machinery for that instance. +- `AgentRecord` — durable control-plane record for the instance while live and + after termination. +- `AgentTurnRecord` — historical record for an individual turn. + +Configurations are immutable and versioned. A running instance stores an +`AgentConfigurationReference` containing: + +- `configurationID` +- `configurationVersion` +- Optional snapshot hash for verification + +The reference is stored on `AgentRecord`, carried through trusted execution +context, recorded on each `AgentTurnRecord`, and retained as memory +provenance. `sessionID + agentID` remains the memory ownership scope; the +configuration reference does not replace the runtime agent identity. + +Editing a configuration creates a new version. Existing instances remain +pinned to their original version unless explicitly refreshed or restarted. + +## Phase 5 — JobSchedulingModule + +Formalize `JobServiceScheduler` behind `JobSchedulingServing`. + +Inject approved ports for: + +- Clock +- Job and schedule storage +- Workflow starting +- Process supervision +- Event publication +- Capability checking +- Guardrail + +Keep schedule creation and schedule execution as separate authorization +decisions. Revalidate Actor, job, workflow, capabilities, and Policy when a +schedule fires. + +`JobKeepAlive` and daemon bootstrap must not create competing scheduling loops. +There must be one authoritative `JobSchedulingModule` per runtime. + +## Phase 6 — Actor capabilities, Guardrail, and SideEffect Broker + +Capabilities define the maximum authority of an Actor: + +- `network.request` +- `ui.present` +- `workflow.start` +- `docker.run` +- `memory.read` +- `memory.write` +- `message.send` + +The order is: + +```text +Capability check +→ Guardrail evaluation +→ Guardrail application +→ SideEffect execution +``` + +Guardrail may restrict or pause a capability but cannot expand the Actor's +capability set. + +The SideEffect Broker centralizes validation, Actor identification, capability +checks, Guardrail, HITL, secret scope, provider selection, audit, and result +normalization. + +## Phase 7 — Network, UI, and secrets + +`NetworkSideEffectModule` validates `http.request`, checks capabilities and +Guardrail, resolves only plugin-declared credentials, attaches generic +Bearer/Basic credentials, applies SSRF rules, and returns sanitized results. + +`UISideEffectModule` validates `ui.present`, checks capabilities and Guardrail, +persists approved HostUI trees, notifies the UI, and returns typed interaction +results. + +Go guests never receive raw secrets or construct native SwiftUI/AppKit. + +Web crawling and file conversion receive narrow, explicit capability profiles. +They do not create broad network or UI permissions for arbitrary Go guests. + +Model provider credentials, service credentials, database credentials, and +plugin HTTP credentials remain separate host-only secret scopes. + +## Phase 8 — Platform modules + +Refactor platform functionality behind focused approved contracts: + +- Database storage contracts by bounded context. +- `SessionMemoryModule` over `MemoryStore`. +- Model client over `AgentModel`, `AgentProvider`, and `HTTPTransport`. +- `MCPClientModule` as transport only. +- `MCPServerModule` as registration, dispatch, validation, and encoding only. + +MCP does not duplicate capability checks or Guardrail evaluation. The +SideEffect Broker is the authority path. + +## Phase 9 — Plugin runtime and system plugins + +User and system plugins use the same manifest, Go guest ABI, SideEffect +envelopes, result protocol, and runtime. + +System/user differences are visibility, creation rights, edit rights, +capability grants, distribution, and upgrade policy. System plugins remain +user-space programs and do not bypass the SideEffect Broker or Guardrail. + +Plugin source, skills, binaries, and packages remain runtime-managed artifacts, +not repository source. + +## Phase 10 — Migration and verification + +Migrate in this order: + +1. Approve contracts. +2. Establish message families. +3. Establish lifecycle and supervision. +4. Consolidate `AgentOrchestrationModule`. +5. Formalize `JobSchedulingModule`. +6. Add Actor capability checks. +7. Add the SideEffect Broker. +8. Add network and UI modules. +9. Isolate secrets. +10. Narrow database and memory dependencies. +11. Simplify model and MCP transport roles. +12. Consolidate plugin runtime boundaries. +13. Migrate callers. +14. Delete bypasses and obsolete types. + +Verify: + +- Agent mailboxes and session memory remain separate. +- Only one scheduler claims due work. +- Scheduler recovery works after interruption. +- Actors cannot exceed capabilities. +- Guardrail cannot expand capabilities. +- Containers cannot make direct network calls or construct native UI. +- Plugin secrets never enter guest input, environment, arguments, or logs. +- Model API keys remain host-only. +- MCP cannot bypass the SideEffect Broker. +- Services, modules, guests, and system plugins use approved contracts. + +## Phase 11 — Final documentation + +Update `docs/Design.md`, this plan, package boundary documentation, and +`AGENTS.md`. + +`AGENTS.md` must include: + +> Never change an existing protocol or add a new one without first asking the +> operator. Never build a module or feature that does not have a protocol +> contract and types. If a new protocol is needed, ask the operator first. diff --git a/packages/AgentRuntime/Sources/AgentRuntime/HierarchicalOrchestrator.swift b/packages/AgentRuntime/Sources/AgentRuntime/HierarchicalOrchestrator.swift index 24c0c671..6ef4e6a1 100644 --- a/packages/AgentRuntime/Sources/AgentRuntime/HierarchicalOrchestrator.swift +++ b/packages/AgentRuntime/Sources/AgentRuntime/HierarchicalOrchestrator.swift @@ -1,12 +1,12 @@ import Foundation import Structure -/// Hierarchical task protocol helpers on top of `AgentDirectorying`. +/// Hierarchical task protocol helpers on top of `AgentRegistryManaging`. /// /// Spawn-and-await runs a worker turn via the supplied executor and returns the result /// to the parent tool call (mailbox + serial turns still apply). -public actor HierarchicalOrchestrator { - private let directory: any AgentDirectorying +public actor HierarchicalOrchestrator: AgentDirecting { + private let directory: any AgentRegistryManaging /// Results reported via `agents_complete_task` during a worker turn (keyed by child ref). private var explicitTaskResults: [AgentRef: String] = [:] @@ -15,7 +15,7 @@ public actor HierarchicalOrchestrator { /// Worker turns currently inside `runTurn` (for complete_task identity when TaskLocal is unavailable across MCP). private var activeWorkerTurns: Set = [] - public init(directory: any AgentDirectorying) { + public init(directory: any AgentRegistryManaging) { self.directory = directory } diff --git a/packages/AgentRuntime/Sources/AgentRuntime/InMemoryAgentDirectory.swift b/packages/AgentRuntime/Sources/AgentRuntime/InMemoryAgentDirectory.swift index daf0c4f5..7ec08ba7 100644 --- a/packages/AgentRuntime/Sources/AgentRuntime/InMemoryAgentDirectory.swift +++ b/packages/AgentRuntime/Sources/AgentRuntime/InMemoryAgentDirectory.swift @@ -2,7 +2,7 @@ import Foundation import Structure /// In-process agent registry + serial mailbox processing (MA-0/MA-1). -public actor InMemoryAgentDirectory: AgentDirectorying { +public actor InMemoryAgentDirectory: AgentRegistryManaging { public let limits: OrchestrationLimits private struct RuntimeSlot { diff --git a/packages/DBRepository/Sources/DBRepository/DBAgentDirectory.swift b/packages/DBRepository/Sources/DBRepository/DBAgentDirectory.swift index 8f69f0b4..cfccc5c0 100644 --- a/packages/DBRepository/Sources/DBRepository/DBAgentDirectory.swift +++ b/packages/DBRepository/Sources/DBRepository/DBAgentDirectory.swift @@ -3,8 +3,8 @@ import AgentRuntime import Structure /// DB-backed agent directory: persists registry rows via `DBRepository` and delegates -/// runtime mailbox / turn concurrency to an in-memory `AgentDirectorying` implementation. -public actor DBAgentDirectory: AgentDirectorying { +/// runtime mailbox / turn concurrency to an in-memory `AgentRegistryManaging` implementation. +public actor DBAgentDirectory: AgentRegistryManaging { public let limits: OrchestrationLimits private let repository: DBRepository diff --git a/packages/Structure/Sources/AgentRuntime/AgentIdentityTypes.swift b/packages/Structure/Sources/AgentRuntime/AgentIdentityTypes.swift new file mode 100644 index 00000000..36bc877d --- /dev/null +++ b/packages/Structure/Sources/AgentRuntime/AgentIdentityTypes.swift @@ -0,0 +1,59 @@ +import Foundation + +/// Broad identity category used when an Actor requests a capability. +public enum ActorKind: String, Codable, Sendable, Hashable, CaseIterable { + case humanOperator = "operator" + case ui + case agent + case pluginGuest = "plugin_guest" + case module + case service + case job + case workflow + case system + case webhook +} + +/// Stable identity for the Actor attempting a command or SideEffect. +public struct ActorID: Codable, Sendable, Hashable, Identifiable { + public let kind: ActorKind + public let value: String + + public var id: String { + "\(kind.rawValue):\(value)" + } + + public init(kind: ActorKind, value: String) { + self.kind = kind + self.value = value + } +} + +/// Runtime category for an AgentInstance. This is separate from AgentRole, +/// AgentStatus, and AgentConfiguration. +public enum AgentKind: String, Codable, Sendable, Hashable, CaseIterable { + case interactive + case delegated + case scheduled + case workflow + case integration + case system +} + +/// Immutable configuration identity attached to an AgentInstance and its +/// historical turns. +public struct AgentConfigurationReference: Codable, Sendable, Hashable { + public let configurationID: String + public let configurationVersion: Int + public let snapshotHash: String? + + public init( + configurationID: String, + configurationVersion: Int, + snapshotHash: String? = nil + ) { + self.configurationID = configurationID + self.configurationVersion = configurationVersion + self.snapshotHash = snapshotHash + } +} diff --git a/packages/Structure/Sources/AgentRuntime/AgentRecord.swift b/packages/Structure/Sources/AgentRuntime/AgentRecord.swift index c74cd4f9..616a561d 100644 --- a/packages/Structure/Sources/AgentRuntime/AgentRecord.swift +++ b/packages/Structure/Sources/AgentRuntime/AgentRecord.swift @@ -26,9 +26,13 @@ public struct AgentRecord: Hashable, Codable, Sendable, Identifiable { public var id: AgentRef { ref } public let ref: AgentRef + /// Runtime category, distinct from hierarchy role and lifecycle status. + public var kind: AgentKind public var role: AgentRole public var parentAgentID: String? public var status: AgentStatus + /// Optional during migration; new orchestration-created instances must set it. + public var configurationReference: AgentConfigurationReference? /// Short role / goal overlay for system prompt composition. public var goal: String? public var systemOverlay: String? @@ -39,9 +43,11 @@ public struct AgentRecord: Hashable, Codable, Sendable, Identifiable { public init( ref: AgentRef, + kind: AgentKind = .interactive, role: AgentRole, parentAgentID: String? = nil, status: AgentStatus = .idle, + configurationReference: AgentConfigurationReference? = nil, goal: String? = nil, systemOverlay: String? = nil, modelPreference: String? = nil, @@ -50,9 +56,11 @@ public struct AgentRecord: Hashable, Codable, Sendable, Identifiable { metadata: [String: String] = [:] ) { self.ref = ref + self.kind = kind self.role = role self.parentAgentID = parentAgentID self.status = status + self.configurationReference = configurationReference self.goal = goal self.systemOverlay = systemOverlay self.modelPreference = modelPreference diff --git a/packages/Structure/Sources/AgentRuntime/Protocols.swift b/packages/Structure/Sources/AgentRuntime/Protocols.swift index d3f0951e..f777da33 100644 --- a/packages/Structure/Sources/AgentRuntime/Protocols.swift +++ b/packages/Structure/Sources/AgentRuntime/Protocols.swift @@ -7,8 +7,12 @@ public protocol AgentMailboxing: Sendable { func peekCount() async -> Int } -/// Registry of agents for a session (or process). -public protocol AgentDirectorying: Sendable { +/// Authoritative registry of runtime agent instances for a session or process. +/// +/// This is the Phase 1 name for the existing registry contract. Message +/// delivery remains here temporarily for source compatibility; the +/// AgentOrchestrationModule will separate registry and directing concerns. +public protocol AgentRegistryManaging: Sendable { var limits: OrchestrationLimits { get async } func record(for ref: AgentRef) async -> AgentRecord? @@ -39,3 +43,26 @@ public protocol AgentDirectorying: Sendable { public protocol TurnRunning: Sendable { func run(envelope: AgentEnvelope) async throws } + +/// Directs runtime agents through delegation, messaging, completion, and +/// cancellation operations. +public protocol AgentDirecting: Sendable { + func spawnAndAwait( + _ request: SpawnWorkerRequest, + runTurn: @escaping @Sendable (_ child: AgentRecord, _ envelope: AgentEnvelope) async throws -> String + ) async throws -> SpawnWorkerResult + + func spawnManyAndAwait( + _ requests: [SpawnWorkerRequest], + runTurn: @escaping @Sendable (_ child: AgentRecord, _ envelope: AgentEnvelope) async throws -> String + ) async throws -> [SpawnWorkerResult] + + func completeTask(worker: AgentRef, result: String) async throws + func listAgents(sessionID: String) async -> [AgentRecord] + func listChildren(of parent: AgentRef) async -> [AgentRecord] + func send(from: AgentRef, toAgentID: String, message: String) async throws + func cancel(agent: AgentRef, by requester: AgentRef) async throws +} + +/// Composite facade implemented by the future AgentOrchestrationModule. +public protocol AgentOrchestrationServing: AgentRegistryManaging, AgentDirecting {} diff --git a/packages/Structure/Sources/AppLayerServices/JobService/JobSchedulingServing.swift b/packages/Structure/Sources/AppLayerServices/JobService/JobSchedulingServing.swift new file mode 100644 index 00000000..edd4f687 --- /dev/null +++ b/packages/Structure/Sources/AppLayerServices/JobService/JobSchedulingServing.swift @@ -0,0 +1,40 @@ +import Foundation + +public enum JobSchedulingStatus: String, Codable, Sendable, Hashable { + case stopped + case starting + case running + case degraded + case failed +} + +public struct JobSchedulingSnapshot: Codable, Sendable, Hashable { + public let status: JobSchedulingStatus + public let observedAt: Date + public let claimedScheduleCount: Int + public let claimedJobCount: Int + public let detail: String? + + public init( + status: JobSchedulingStatus, + observedAt: Date = .now, + claimedScheduleCount: Int = 0, + claimedJobCount: Int = 0, + detail: String? = nil + ) { + self.status = status + self.observedAt = observedAt + self.claimedScheduleCount = claimedScheduleCount + self.claimedJobCount = claimedJobCount + self.detail = detail + } +} + +/// Contract for the JobSchedulingModule hosted by the current job runtime. +public protocol JobSchedulingServing: Sendable { + func start() async throws + func stop() async + func recoverInterruptedWork() async throws -> Int + func runOnce(at date: Date) async throws -> JobSchedulingSnapshot + func status() async -> JobSchedulingSnapshot +} diff --git a/packages/Structure/Sources/Capability/CapabilityContracts.swift b/packages/Structure/Sources/Capability/CapabilityContracts.swift new file mode 100644 index 00000000..31884a94 --- /dev/null +++ b/packages/Structure/Sources/Capability/CapabilityContracts.swift @@ -0,0 +1,60 @@ +import Foundation + +/// Stable authority name requested by an Actor. +public struct CapabilityID: RawRepresentable, Codable, Sendable, Hashable, + ExpressibleByStringLiteral +{ + public let rawValue: String + + public init(rawValue: String) { + self.rawValue = rawValue + } + + public init(stringLiteral value: String) { + self.init(rawValue: value) + } +} + +public struct CapabilitySet: Codable, Sendable, Hashable { + public let values: Set + + public init(_ values: Set = []) { + self.values = values + } + + public func contains(_ capability: CapabilityID) -> Bool { + values.contains(capability) + } +} + +public struct CapabilityRequest: Codable, Sendable, Hashable { + public let actor: ActorID + public let capability: CapabilityID + public let resource: String? + public let sessionID: String? + public let workflowID: String? + + public init( + actor: ActorID, + capability: CapabilityID, + resource: String? = nil, + sessionID: String? = nil, + workflowID: String? = nil + ) { + self.actor = actor + self.capability = capability + self.resource = resource + self.sessionID = sessionID + self.workflowID = workflowID + } +} + +public enum CapabilityDecision: Codable, Sendable, Hashable { + case allowed + case denied(reason: String) +} + +/// Capability authority used before a Guardrail decision is evaluated. +public protocol ActorCapabilityChecking: Sendable { + func check(_ request: CapabilityRequest) async throws -> CapabilityDecision +} diff --git a/packages/Structure/Sources/ProcessSupervision/ProcessSupervisionContracts.swift b/packages/Structure/Sources/ProcessSupervision/ProcessSupervisionContracts.swift new file mode 100644 index 00000000..112826f0 --- /dev/null +++ b/packages/Structure/Sources/ProcessSupervision/ProcessSupervisionContracts.swift @@ -0,0 +1,70 @@ +import Foundation + +public struct ProcessLaunchRequest: Codable, Sendable, Hashable { + public let processID: String + public let executable: String + public let arguments: [String] + public let workingDirectory: String? + public let timeoutNanoseconds: UInt64? + + public init( + processID: String, + executable: String, + arguments: [String] = [], + workingDirectory: String? = nil, + timeoutNanoseconds: UInt64? = nil + ) { + self.processID = processID + self.executable = executable + self.arguments = arguments + self.workingDirectory = workingDirectory + self.timeoutNanoseconds = timeoutNanoseconds + } +} + +public enum ProcessStatus: String, Codable, Sendable, Hashable { + case created + case starting + case running + case stopping + case stopped + case failed + case timedOut +} + +public struct ProcessHandle: Codable, Sendable, Hashable, Identifiable { + public let id: String + public let launchedAt: Date + + public init(id: String, launchedAt: Date = .now) { + self.id = id + self.launchedAt = launchedAt + } +} + +public struct ProcessStatusSnapshot: Codable, Sendable, Hashable { + public let handle: ProcessHandle + public let status: ProcessStatus + public let exitCode: Int32? + public let detail: String? + + public init( + handle: ProcessHandle, + status: ProcessStatus, + exitCode: Int32? = nil, + detail: String? = nil + ) { + self.handle = handle + self.status = status + self.exitCode = exitCode + self.detail = detail + } +} + +/// Supervises standalone services, guest processes, and helper processes. +public protocol ProcessSupervising: Sendable { + func launch(_ request: ProcessLaunchRequest) async throws -> ProcessHandle + func stop(_ handle: ProcessHandle, reason: String?) async + func cancel(_ handle: ProcessHandle, reason: String?) async + func status(for handle: ProcessHandle) async -> ProcessStatusSnapshot? +} diff --git a/packages/Structure/Sources/SideEffect/SideEffectContracts.swift b/packages/Structure/Sources/SideEffect/SideEffectContracts.swift new file mode 100644 index 00000000..5ceb1469 --- /dev/null +++ b/packages/Structure/Sources/SideEffect/SideEffectContracts.swift @@ -0,0 +1,121 @@ +import Foundation + +public struct NetworkSideEffectRequest: Codable, Sendable, Hashable { + public let actor: ActorID + public let correlationID: String + public let request: HostHTTPRequest + + public init( + actor: ActorID, + correlationID: String, + request: HostHTTPRequest + ) { + self.actor = actor + self.correlationID = correlationID + self.request = request + } +} + +public struct NetworkSideEffectResult: Codable, Sendable, Hashable { + public let response: HostHTTPResponse + + public init(response: HostHTTPResponse) { + self.response = response + } +} + +public protocol NetworkSideEffectExecuting: Sendable { + func execute(_ request: NetworkSideEffectRequest) async throws -> NetworkSideEffectResult +} + +public struct UISideEffectRequest: Codable, Sendable, Hashable { + public let actor: ActorID + public let correlationID: String + public let pluginID: String + public let root: HostUINode + + public init( + actor: ActorID, + correlationID: String, + pluginID: String, + root: HostUINode + ) { + self.actor = actor + self.correlationID = correlationID + self.pluginID = pluginID + self.root = root + } +} + +public struct UISideEffectResult: Codable, Sendable, Hashable { + public let accepted: Bool + public let presentationID: String? + + public init(accepted: Bool, presentationID: String? = nil) { + self.accepted = accepted + self.presentationID = presentationID + } +} + +public protocol UISideEffectExecuting: Sendable { + func execute(_ request: UISideEffectRequest) async throws -> UISideEffectResult +} + +public enum HostSideEffectPayload: Codable, Sendable, Hashable { + case network(NetworkSideEffectRequest) + case ui(UISideEffectRequest) +} + +public struct HostSideEffectRequest: Codable, Sendable, Hashable { + public let payload: HostSideEffectPayload + + public init(payload: HostSideEffectPayload) { + self.payload = payload + } +} + +public enum HostSideEffectResultPayload: Codable, Sendable, Hashable { + case network(NetworkSideEffectResult) + case ui(UISideEffectResult) +} + +public struct HostSideEffectResult: Codable, Sendable, Hashable { + public let payload: HostSideEffectResultPayload + + public init(payload: HostSideEffectResultPayload) { + self.payload = payload + } +} + +/// Central host authority that validates and routes approved SideEffects. +public protocol HostSideEffectExecuting: Sendable { + func execute(_ request: HostSideEffectRequest) async throws -> HostSideEffectResult +} + +public enum HostSecretScope: String, Codable, Sendable, Hashable, CaseIterable { + case pluginHTTP = "plugin_http" + case modelProvider = "model_provider" + case serviceAuthentication = "service_authentication" + case database +} + +public struct HostSecretReference: Codable, Sendable, Hashable { + public let scope: HostSecretScope + public let ownerID: String + public let fieldID: String + + public init(scope: HostSecretScope, ownerID: String, fieldID: String) { + self.scope = scope + self.ownerID = ownerID + self.fieldID = fieldID + } +} + +/// Attaches a host-owned secret to a host request without exposing the value +/// to a plugin guest. +public protocol HostSecretAttaching: Sendable { + func attach( + _ request: HostHTTPRequest, + using reference: HostSecretReference + ) async throws -> HostHTTPRequest +} diff --git a/packages/Structure/Tests/StructureTests/CoreRedesignContractTests.swift b/packages/Structure/Tests/StructureTests/CoreRedesignContractTests.swift new file mode 100644 index 00000000..40d473e6 --- /dev/null +++ b/packages/Structure/Tests/StructureTests/CoreRedesignContractTests.swift @@ -0,0 +1,72 @@ +import Foundation +import Testing +@testable import Structure + +@Suite("Core redesign contracts") +struct CoreRedesignContractTests { + @Test func actorAndAgentKindsRemainDistinct() { + #expect(ActorKind.agent != ActorKind.pluginGuest) + #expect(AgentKind.interactive != AgentKind.delegated) + #expect(AgentRole.userFacing != AgentRole.worker) + } + + @Test func agentRecordCarriesConfigurationReference() { + let reference = AgentConfigurationReference( + configurationID: "generalist", + configurationVersion: 3, + snapshotHash: "hash" + ) + let record = AgentRecord( + ref: AgentRef(sessionID: "session", agentID: "agent"), + kind: .interactive, + role: .userFacing, + configurationReference: reference + ) + + #expect(record.configurationReference == reference) + #expect(record.kind == .interactive) + } + + @Test func capabilityRequestIdentifiesActor() { + let request = CapabilityRequest( + actor: ActorID(kind: .pluginGuest, value: "connector"), + capability: "network.request", + resource: "https://example.com", + sessionID: "session" + ) + + #expect(request.actor.kind == .pluginGuest) + #expect(request.capability.rawValue == "network.request") + } + + @Test func processLaunchRequestIsTyped() { + let request = ProcessLaunchRequest( + processID: "guest-1", + executable: "/usr/bin/guest", + arguments: ["--input"], + timeoutNanoseconds: 10 + ) + + #expect(request.processID == "guest-1") + #expect(request.arguments == ["--input"]) + #expect(request.timeoutNanoseconds == 10) + } + + @Test func sideEffectRequestsRetainActorContext() throws { + let request = HostHTTPRequest( + requestID: "request-1", + method: "GET", + url: "https://example.com" + ) + let sideEffect = NetworkSideEffectRequest( + actor: ActorID(kind: .pluginGuest, value: "connector"), + correlationID: "correlation-1", + request: request + ) + let data = try JSONEncoder().encode(sideEffect) + let decoded = try JSONDecoder().decode(NetworkSideEffectRequest.self, from: data) + + #expect(decoded.actor.kind == .pluginGuest) + #expect(decoded.request.url == "https://example.com") + } +} diff --git a/ui/SharedAgentRuntime/Orchestration/SessionOrchestrator.swift b/ui/SharedAgentRuntime/Orchestration/SessionOrchestrator.swift index 66c1261c..00d50ba4 100644 --- a/ui/SharedAgentRuntime/Orchestration/SessionOrchestrator.swift +++ b/ui/SharedAgentRuntime/Orchestration/SessionOrchestrator.swift @@ -7,7 +7,7 @@ import Structure /// Session-scoped multi-agent entry point (MA-1–MA-3). final class SessionOrchestrator: Sendable { let sessionID: String - let directory: any AgentDirectorying + let directory: any AgentRegistryManaging let hierarchy: HierarchicalOrchestrator let limits: OrchestrationLimits let userFacingRef: AgentRef @@ -17,7 +17,7 @@ final class SessionOrchestrator: Sendable { init( sessionID: String, - directory: any AgentDirectorying, + directory: any AgentRegistryManaging, limits: OrchestrationLimits = OrchestrationLimitsRuntime.current ) { self.sessionID = sessionID