syntax = "proto3";

package iop;

import "google/protobuf/struct.proto";

option go_package = "iop/proto/gen/iop";

// RunRequest initiates an adapter execution on a node.
message RunRequest {
  reserved 4, 10;
  reserved "workspace", "session_mode";
  string run_id                  = 1;
  string adapter                 = 2;
  string target                  = 3;
  google.protobuf.Struct policy  = 5;
  google.protobuf.Struct input   = 6;
  int32  timeout_sec             = 7;
  map<string, string> metadata   = 8;
  string session_id              = 9;
  bool background                = 11;
  // response_stall_timeout_ms is the selected provider's response-stall
  // timeout in milliseconds. Zero means the Node applies the documented
  // default (300000). Negative or overflow values are rejected at the Node
  // boundary before router/provider invocation.
  int64  response_stall_timeout_ms = 12;
}

// RunEvent is a streaming execution event.
message RunEvent {
  string run_id    = 1;
  string type      = 2; // start | delta | complete | error | cancelled
  string delta     = 3;
  string message   = 4;
  string error     = 5;
  Usage  usage     = 6;
  map<string, string> metadata = 7;
  int64  timestamp = 8; // unix nano
  string session_id = 9;
  bool background   = 10;
  string node_id    = 11;
  string node_alias = 12;
  ExecutionFailure failure = 13;
}

enum ProviderTunnelFrameKind {
  PROVIDER_TUNNEL_FRAME_KIND_UNSPECIFIED    = 0;
  PROVIDER_TUNNEL_FRAME_KIND_RESPONSE_START = 1;
  PROVIDER_TUNNEL_FRAME_KIND_BODY           = 2;
  PROVIDER_TUNNEL_FRAME_KIND_END            = 3;
  PROVIDER_TUNNEL_FRAME_KIND_ERROR          = 4;
  PROVIDER_TUNNEL_FRAME_KIND_USAGE          = 5;
}

// ProviderTunnelRequest asks a node to open a provider HTTP request and relay
// the raw provider response over ProviderTunnelFrame messages on the existing
// Edge-Node socket. It is separate from RunRequest, which remains the
// normalized adapter execution path.
message ProviderTunnelRequest {
  string run_id                = 1;
  string tunnel_id             = 2;
  string adapter               = 3;
  string target                = 4;
  string method                = 5;
  string path                  = 6;
  map<string, string> headers  = 7;
  bytes body                   = 8;
  bool stream                  = 9;
  int32 timeout_sec            = 10;
  map<string, string> metadata = 11;
  string session_id            = 12;
  // operation is the protocol operation id (e.g. "chat_completions",
  // "messages", "models"). When set, the Node adapter resolves the request URL
  // from the concrete profile's operation path. When empty, the legacy Path
  // field is used as a mixed-version fallback.
  string operation             = 13;
  // credential_lease is a signed, recipient-sealed credential envelope. It is
  // deliberately separate from headers/metadata so protobuf debug output and
  // generic forwarding paths cannot expose plaintext provider credentials.
  SignedCredentialLease credential_lease = 14;
  // credential_binding is the independently resolved Edge dispatch binding
  // the Node compares byte-for-byte with the signed lease before consumption.
  CredentialLeaseBinding credential_binding = 15;
  // response_stall_timeout_ms is the selected provider's response-stall
  // timeout in milliseconds. Zero means the Node applies the documented
  // default (300000). Negative or overflow values are rejected at the Node
  // boundary before router/provider invocation.
  int64  response_stall_timeout_ms = 16;
}

message CredentialLeaseScope {
  string lease_id = 1;
  string principal_ref = 2;
  string credential_slot_ref = 3;
  string route_id = 4;
  string profile_id = 5;
  string upstream_target = 6;
  string node_id = 7;
  string recipient_key_id = 8;
  string header_name = 9;
  string scheme = 10;
  uint64 credential_revision = 11;
  uint64 route_revision = 12;
  uint64 projection_generation = 13;
  int64 issued_at_unix_nano = 14;
  int64 expires_at_unix_nano = 15;
}

message SignedCredentialLease {
  uint32 version = 1;
  string issuer_key_id = 2;
  CredentialLeaseScope scope = 3;
  bytes ephemeral_public_key = 4;
  bytes nonce = 5;
  bytes ciphertext = 6;
  bytes signature = 7;
}

message CredentialLeaseBinding {
  string principal_ref = 1;
  string credential_slot_ref = 2;
  string route_id = 3;
  string profile_id = 4;
  string upstream_target = 5;
  string node_id = 6;
  string recipient_key_id = 7;
  uint64 credential_revision = 8;
  uint64 route_revision = 9;
  uint64 projection_generation = 10;
}

// AcquireLeaseRequest is sent only over the authenticated Control Plane-Edge
// channel after an exact Node candidate is chosen. It is a runtime delivery
// operation, not a credential-management or bootstrap API.
message AcquireLeaseRequest {
  string edge_id = 1;
  CredentialLeaseBinding binding = 2;
  bytes recipient_public_key = 3;
}

message AcquireLeaseResponse {
  SignedCredentialLease lease = 1;
  string error = 2;
}

// ProviderTunnelFrame carries ordered raw provider response data back to Edge.
// Body bytes are the passthrough source of truth and must not be routed through
// RunEvent.delta or the lossy event bus fanout.
message ProviderTunnelFrame {
  string run_id                = 1;
  string tunnel_id             = 2;
  int64 sequence               = 3;
  ProviderTunnelFrameKind kind = 4;
  int32 status_code            = 5;
  map<string, string> headers  = 6;
  bytes body                   = 7;
  bool end                     = 8;
  string error                 = 9;
  Usage usage                  = 10;
  map<string, string> metadata = 11;
  int64 timestamp              = 12; // unix nano
  string node_id               = 13;
  string node_alias            = 14;
  ExecutionFailure failure     = 15;
}

// EdgeNodeEvent is a general edge-node lifecycle/control event envelope.
// It is separate from RunEvent, which is reserved for adapter execution streams.
message EdgeNodeEvent {
  string event_id  = 1;
  string type      = 2; // node.connected | node.disconnected | edge.disconnected | ...
  string source    = 3; // edge | node
  string node_id   = 4;
  string alias     = 5;
  string reason    = 6;
  map<string, string> metadata = 7;
  int64 timestamp = 8; // unix nano
}

// ExecutionFailure is the typed failure payload carried by execution envelopes.
message ExecutionFailure {
  string code                  = 1;
  string message               = 2;
  bool   retryable             = 3;
  map<string, string> metadata = 4;
}

message Usage {
  int32 input_tokens  = 1;
  int32 output_tokens = 2;
  // reasoning_tokens and cached_input_tokens are populated only when the
  // provider reports them. A zero value means "not reported"; downstream metric
  // emit never estimates reasoning tokens from observed reasoning text.
  int32 reasoning_tokens     = 3;
  int32 cached_input_tokens  = 4;
}

// Heartbeat is sent by both sides to keep the connection alive.
message Heartbeat {
  int64 timestamp = 1;
}

// CancelRequest asks the node to cancel a running execution.
message CancelRequest {
  reserved 2, 3, 4, 5;
  reserved "adapter", "target", "session_id", "action";
  string run_id = 1;
}

enum NodeCommandType {
  reserved 1, 3;
  reserved "NODE_COMMAND_TYPE_USAGE_STATUS", "NODE_COMMAND_TYPE_SESSION_LIST";
  NODE_COMMAND_TYPE_UNSPECIFIED      = 0;
  NODE_COMMAND_TYPE_CAPABILITIES     = 2;
  NODE_COMMAND_TYPE_TRANSPORT_STATUS = 4;
  NODE_COMMAND_TYPE_OLLAMA_API       = 5;
}

message NodeCommandRequest {
  string request_id              = 1;
  NodeCommandType type           = 2;
  string adapter                 = 3;
  string target                  = 4;
  string session_id              = 5;
  int32 timeout_sec              = 6;
  map<string, string> metadata   = 7;
}

message NodeCommandResponse {
  reserved 6;
  reserved "usage_status";
  string request_id              = 1;
  NodeCommandType type           = 2;
  string adapter                 = 3;
  string target                  = 4;
  string session_id              = 5;
  string error                   = 7;
  // result carries free-form key/value data for provider-only commands.
  map<string, string> result     = 8;
  repeated ProviderSnapshot provider_snapshots = 9;
}

// ProviderSnapshot is the stable wire name for a node provider status snapshot.
message ProviderSnapshot {
  string adapter    = 1;
  string status     = 2; // unknown|available|unavailable
  int32  capacity   = 3;
  int32  in_flight  = 4;
  int32  queued     = 5;

  // Provider catalog fields (MVP).
  // id uniquely identifies this provider within its node.
  string id              = 6;
  // type is the runtime type (e.g. "ollama", "vllm", "lemonade", "sglang", "openai_api").
  string type            = 7;
  // category classifies the provider; values: api, local_inference.
  string category        = 8;
  // served_models lists the model names this provider can actually serve.
  repeated string served_models = 9;
  // health is the observed provider health state (e.g. "healthy", "degraded", "unhealthy").
  string health          = 10;
  // load_ratio is the computed in_flight / capacity value.  When capacity is 0
  // or unknown the provider is treated as unavailable for selection.
  float  load_ratio      = 11;
  // lifecycle_capabilities lists coarse lifecycle capability flags (e.g.
  // "list_models", "load_model", "unload_model", "pull_model", "delete_model").
  repeated string lifecycle_capabilities = 12;

  // Long-context fields.
  int32 long_context_capacity = 13;
  int32 long_in_flight = 14;
  int32 long_queued = 15;
}


// Error is returned when a request fails at the transport layer.
message Error {
  string code    = 1;
  string message = 2;
}

// RegisterRequest is sent by node to edge immediately on connect.
message RegisterRequest {
  string token = 1;
  string credential_recipient_key_id = 2;
  bytes credential_recipient_public_key = 3;
}

// RegisterResponse is sent by edge to node in response to RegisterRequest.
message RegisterResponse {
  bool              accepted = 1;
  string            node_id  = 2;
  string            alias    = 3;
  string            reason   = 4; // rejection reason
  NodeConfigPayload config   = 5;
}

// NodeReadyRequest is sent by node to edge after it has applied the config from
// RegisterResponse and installed its message handler, signalling that it can now
// receive run/tunnel/command dispatch. Accepted registration only claims
// ownership and delivers config; edge opens dispatch eligibility and pumps the
// node's stranded waiters only on this ready handshake, never before. node_id
// carries the identity edge assigned in RegisterResponse so the ready transition
// binds to the exact accepted connection.
message NodeReadyRequest {
  string node_id = 1;
}

// NodeReadyResponse acknowledges a NodeReadyRequest. ready is true once edge has
// marked the connection dispatch-ready (idempotent for the current owner); it is
// false when the connection is stale — superseded by a reconnect or already gone
// — in which case the node closes and reconnects. reason describes a rejection.
message NodeReadyResponse {
  bool   ready  = 1;
  string reason = 2;
}

// NodeConfigPayload carries all configuration edge pushes to the node.
message NodeConfigPayload {
  repeated AdapterConfig    adapters = 1;
  NodeRuntimeConfig         runtime  = 2;
  // workspaces is the Node-private, operator-approved workspace capability
  // catalog. It is deliberately separate from RunRequest metadata and from
  // the closed NodeCommand surface.
  repeated WorkspaceConfig  workspaces = 3;
}

// WorkspaceOperation is the closed set of workspace operations admitted by
// Edge and implemented by the Node-private executor.
enum WorkspaceOperation {
  WORKSPACE_OPERATION_UNSPECIFIED = 0;
  WORKSPACE_OPERATION_READ        = 1;
  WORKSPACE_OPERATION_LIST        = 2;
  WORKSPACE_OPERATION_WRITE       = 3;
  WORKSPACE_OPERATION_DELETE      = 4;
  WORKSPACE_OPERATION_COMMAND     = 5;
}

message WorkspaceCommandConfig {
  string id = 1;
  string executable = 2;
  repeated string args = 3;
}

// WorkspaceConfig is delivered only inside the Edge-owned Node config payload.
// Roots, command templates, and environment names never appear in public API
// responses or in a caller-selected request field.
message WorkspaceConfig {
  string ref = 1;
  string platform = 2;
  string root = 3;
  repeated WorkspaceOperation operations = 4;
  repeated WorkspaceCommandConfig commands = 5;
  repeated string environment_allowlist = 6;
  int64 max_read_bytes = 7;
  int64 max_write_bytes = 8;
  int64 max_output_bytes = 9;
  int64 max_command_timeout_ms = 10;
}

enum WorkspaceStatus {
  WORKSPACE_STATUS_UNSPECIFIED = 0;
  WORKSPACE_STATUS_SUCCESS     = 1;
  WORKSPACE_STATUS_ERROR       = 2;
  WORKSPACE_STATUS_TIMEOUT     = 3;
  WORKSPACE_STATUS_CANCELLED   = 4;
  WORKSPACE_STATUS_UNSUPPORTED = 5;
}

enum WorkspaceErrorCode {
  WORKSPACE_ERROR_CODE_UNSPECIFIED    = 0;
  WORKSPACE_ERROR_CODE_NOT_READY      = 1;
  WORKSPACE_ERROR_CODE_UNSUPPORTED    = 2;
  WORKSPACE_ERROR_CODE_INVALID_REQUEST = 3;
  WORKSPACE_ERROR_CODE_NOT_FOUND      = 4;
  WORKSPACE_ERROR_CODE_TIMEOUT        = 5;
  WORKSPACE_ERROR_CODE_CANCELLED      = 6;
  WORKSPACE_ERROR_CODE_INTERNAL       = 7;
}

// WorkspaceOpenRequest begins one request-owned workspace lifecycle. request_id
// is the immutable coordinator identity and later names .iop/job/<request_id>.
message WorkspaceOpenRequest {
  string request_id = 1;
  string workspace_ref = 2;
  int64 timeout_ms = 3;
  repeated WorkspaceOperation operations = 4;
  repeated string command_ids = 5;
  int64 max_read_bytes = 6;
  int64 max_write_bytes = 7;
  int64 max_output_bytes = 8;
  int64 max_command_timeout_ms = 9;
}

message WorkspaceOpenResponse {
  string request_id = 1;
  string workspace_ref = 2;
  WorkspaceStatus status = 3;
  WorkspaceErrorCode error_code = 4;
  string error = 5;
}

message WorkspaceWriteInput {
  string relative_path = 1;
  bytes content = 2;
}

// WorkspaceToolRequest carries only closed operation input. A caller cannot
// select a Node, root, executable, argv, or arbitrary environment.
message WorkspaceToolRequest {
  string request_id = 1;
  string stage_id = 2;
  string tool_call_id = 3;
  WorkspaceOperation operation = 4;
  int64 timeout_ms = 5;
  oneof input {
    string relative_path = 6;
    // Legacy source/wire-compatible field. WRITE requires the structured
    // write input because this field cannot carry a destination path.
    bytes write_content = 7;
    string command_id = 8;
    WorkspaceWriteInput write = 10;
  }
  map<string, string> environment = 9;
}

message WorkspaceToolResponse {
  string request_id = 1;
  string stage_id = 2;
  string tool_call_id = 3;
  WorkspaceStatus status = 4;
  WorkspaceErrorCode error_code = 5;
  string error = 6;
  bytes content = 7;
  repeated string entries = 8;
  bytes stdout = 9;
  bytes stderr = 10;
  int32 exit_code = 11;
  bool truncated = 12;
  int64 duration_ms = 13;
}

// WorkspaceArtifactKind is a closed coordinator-only artifact selector. Node
// maps these values to fixed names inside .iop/job/<request_id>; no path crosses
// the wire or becomes available to public workspace tools.
enum WorkspaceArtifactKind {
  WORKSPACE_ARTIFACT_KIND_UNSPECIFIED = 0;
  WORKSPACE_ARTIFACT_KIND_PLAN        = 1;
  WORKSPACE_ARTIFACT_KIND_REVIEW      = 2;
}

enum WorkspaceArtifactOperation {
  WORKSPACE_ARTIFACT_OPERATION_UNSPECIFIED = 0;
  WORKSPACE_ARTIFACT_OPERATION_READ        = 1;
  WORKSPACE_ARTIFACT_OPERATION_WRITE       = 2;
}

message WorkspaceArtifactRequest {
  string request_id = 1;
  WorkspaceArtifactKind kind = 2;
  WorkspaceArtifactOperation operation = 3;
  bytes content = 4;
}

message WorkspaceArtifactResponse {
  string request_id = 1;
  WorkspaceArtifactKind kind = 2;
  WorkspaceArtifactOperation operation = 3;
  WorkspaceStatus status = 4;
  WorkspaceErrorCode error_code = 5;
  string error = 6;
  bytes content = 7;
}

message WorkspaceCancelRequest {
  string request_id = 1;
  string stage_id = 2;
  string tool_call_id = 3;
}

message WorkspaceCancelResponse {
  string request_id = 1;
  string stage_id = 2;
  string tool_call_id = 3;
  WorkspaceStatus status = 4;
  WorkspaceErrorCode error_code = 5;
  string error = 6;
}

// WorkspaceCleanupRequest is explicit and request-owned. It removes only
// request artifacts/processes; user workspace results remain outside cleanup.
message WorkspaceCleanupRequest {
  string request_id = 1;
}

message WorkspaceCleanupResponse {
  string request_id = 1;
  WorkspaceStatus status = 2;
  WorkspaceErrorCode error_code = 3;
  string error = 4;
  int32 cleaned_processes = 5;
  int32 cleaned_artifacts = 6;
}

// AdapterConfig describes one adapter to enable on the node.
// name is the stable instance identity within a node; for single-instance
// adapters it may be empty (equivalent to the type name). When a node carries
// multiple instances of the same adapter type each must have a unique name.
message AdapterConfig {
  reserved 4;
  reserved "cli";
  string                 type     = 1; // "mock" | "ollama" | "vllm" | "openai_compat"
  bool                   enabled  = 2;
  google.protobuf.Struct settings = 3; // legacy/compat path; new adapters use oneof
  oneof config {
    OllamaAdapterConfig       ollama        = 5;
    VllmAdapterConfig         vllm          = 6;
    MockAdapterConfig         mock          = 7;
    OpenAICompatAdapterConfig openai_compat = 10;
  }
  string name   = 8; // stable instance identity; empty = legacy single-instance
  string target = 9; // optional default target/route hint for this instance
}

message MockAdapterConfig {}

message OllamaAdapterConfig {
  string base_url           = 1;
  int32  context_size       = 2;
  int32  capacity           = 3;
  int32  max_queue          = 4;
  int32  queue_timeout_ms   = 5;
  int32  request_timeout_ms = 6;
}

message VllmAdapterConfig {
  string endpoint           = 1;
  int32  capacity           = 2;
  int32  max_queue          = 3;
  int32  queue_timeout_ms   = 4;
  int32  request_timeout_ms = 5;
}

message OpenAICompatAdapterConfig {
  string provider             = 1;
  string endpoint             = 2;
  map<string, string> headers = 3;
  int32  capacity             = 4;
  int32  max_queue            = 5;
  int32  queue_timeout_ms     = 6;
  int32  request_timeout_ms   = 7;
  // protocol_profile is the resolved concrete protocol profile snapshot.
  // When set, the adapter uses the profile's operation paths for URL
  // resolution instead of the legacy endpoint + /v1 suffix heuristic.
  ConcreteProtocolProfile protocol_profile = 8;
}

// ProtocolAuth declares how a concrete protocol profile authenticates to its
// upstream. It is the wire representation of config.ProtocolAuthConf.
message ProtocolAuth {
  string header = 1; // request header name (e.g. "Authorization", "x-api-key")
  string scheme = 2; // auth scheme prefix (e.g. "Bearer"); empty means raw header value
}

// ConcreteProtocolProfile is the immutable, resolved snapshot of a protocol
// profile carried over the wire. It never contains a Base reference.
message ConcreteProtocolProfile {
  string id = 1;
  string driver = 2;
  string base_url = 3;
  map<string, string> operations = 4;
  ProtocolAuth auth = 5;
  repeated string capabilities = 6;
  map<string, string> model_mapping = 7;
  google.protobuf.Struct extensions = 8;
}

// NodeRuntimeConfig carries legacy node runtime metadata. Execution admission
// must not use this as a node-wide global gate; provider/resource capacity owns
// concurrency.
message NodeRuntimeConfig {
  int32 concurrency = 1; // legacy compatibility, 0/unset means no node-wide limit
  reserved 2;
}

enum NodeConfigRefreshStatus {
  NODE_CONFIG_REFRESH_STATUS_UNSPECIFIED     = 0;
  NODE_CONFIG_REFRESH_STATUS_APPLIED         = 1;
  NODE_CONFIG_REFRESH_STATUS_RESTART_REQUIRED = 2;
  NODE_CONFIG_REFRESH_STATUS_FAILED          = 3;
  NODE_CONFIG_REFRESH_STATUS_SKIPPED         = 4;
}

// NodeConfigRefreshRequest is sent by edge to a connected node to push a new config payload.
message NodeConfigRefreshRequest {
  string            request_id    = 1;
  NodeConfigPayload config        = 2;
  repeated string   changed_paths = 3;
}

// NodeConfigRefreshResponse is returned by the node to acknowledge or report failure.
message NodeConfigRefreshResponse {
  string                  request_id             = 1;
  NodeConfigRefreshStatus status                 = 2;
  repeated string         restart_required_paths = 3;
  string                  error                  = 4;
}
