Skip to content

Latest commit

 

History

History
110 lines (82 loc) · 11.3 KB

CONTRACTS.md

File metadata and controls

110 lines (82 loc) · 11.3 KB

Contracts — IPC rules and plugin spec

Message definitions live in proto/postflowx/**.proto (source of truth; drafts today — finalized while building Steps 3, 8–12; frozen after Step 12). This file holds the rules around them. Structure: ARCHITECTURE.md.

Not decided yet (by design): the IPC request envelope. Its exact shape (request id, sender, target, operation, payload, deadline, actor, idempotency key, error model, streaming) is designed in ROADMAP Step 3a and then written into this file and proto/. Everything below defines the rules that envelope must serve.

1. Channels and services

Channel Transport Services / content
web → Gateway gRPC / TCP / mTLS operations by domain: auth, users, plugins (admin), approvals, accounts, media, posts, jobs, system (added per step; packages postflowx.<domain>.v1)
process ↔ Gateway ↔ process gRPC / UDS (envelope from Step 3a) typed operations: DB ops (served by the Gateway), fs.* (File-System), plugins.* (PMP), executioner.* (jobs, plugin control, approvals), watchdog.* (status), health
Executioner ↔ plugin private inherited channel plugin.v1.PluginService (Executioner → plugin) and host.v1.HostService (plugin → Executioner)
Controller ↔ Watchdog private socketpair controller.v1: RestartProcess, StopProcess, GetStatus + heartbeat (static platform-process list only)
Watchdog → Gateway health status-only socket grpc.health.v1.Health of the Gateway (it cannot be asked through itself)

2. Identity, routes, limits

  • web↔Gateway: internal CA from init-secrets.sh, server SAN server, client CN=postflowx-web; production mode refuses to start without mTLS. Inside the server: each process has its own socket dir (only that UID + the Gateway UID can enter) — the socket a message arrives on is the sender identity.
  • The Gateway assigns the sender, strips any identity fields supplied by callers, and checks the route table (deny by default). User identity is never read from a caller-controlled field: web sends an opaque session token, the Gateway resolves the actor; plugins can never name a user (§3).
  • Initial route table (finalized in Step 3a): web → Gateway-served ops (auth/users/plugins/approvals/accounts/media/posts/jobs/system) · executioner → fs.*, plugins.*, DB ops, approvals, health replies · pmp → DB ops (+ fs.* if needed) · filesystem → DB ops (metadata) · watchdog → health of every process, status publish · controller is not a Gateway client.
  • Limits (defaults): message 4 MiB (files: chunks of 64 KiB); deadline 30 s unary, Publish 120 s default / 10 min max; per-sender rate + concurrency limits. Invalid identity, unknown route, unknown major version or malformed message → rejected.
  • Versioning: package carries the major (…v1); only additive changes inside a major; breaking = new v2 served in parallel. Plugins declare api_compatibility; the registry refuses incompatible ones. Contract version = deploy-repo tag.

3. Call context (user context for plugins)

Every user-scoped work item (connect, callback, validate, publish, reconcile, per-account task) is initiated by the Executioner, which creates a call context: call_id → (plugin, user, purpose, optional job/account, expiry ≤ call deadline). The plugin echoes call_id on every request; the Executioner looks up the context, attaches the actor when forwarding to the Gateway, and rejects unknown, expired or foreign call_ids. Disabling a plugin or revoking a grant drops its contexts. Plugin-global operations need only the plugin identity + a grant.

4. Host API (plugin → Executioner → Gateway)

Every call is first rights-checked by the Executioner (§5), then forwarded.

Group Operations Notes
Storage StorageGet/Put/Delete/List scope USER (owner from the call context) or PLUGIN; Put with expected_version; value ≤ 256 KiB; per-plugin quota
Accounts UpsertAccount, DeleteAccount, SetAccountStatus only for the context's user + this plugin
Credentials StoreCredential, LeaseCredential lease returns plaintext + lease_expires_at; audited (plugin, user, account, purpose)
Media OpenMedia (stream), CreatePublicMediaUrl only media of the current job; served by File-System; TTL capped
Jobs CheckpointAttempt opaque bytes, size-limited, job-scoped context only
Audit AppendAudit actor/plugin set by the platform; no secrets

5. Rights check and local approval (Executioner)

Plugin trust level Capability granted? Result
trusted yes forwarded silently
trusted no PERMISSION_DENIED
untrusted yes held → approval needed
any not granted PERMISSION_DENIED

Approval is fully local (no external server, no network): the Executioner creates an approval entry (approvals.create, stored in the local DB: plugin, capability, summary of the request, call context, time). The admin answers deny / allow once / allow always for this capability via the web UI (the BFF follows approvals.pending through the Gateway and pushes to the browser with SSE) or via postflowx-admin approvals list|allow|deny. The decision is stored and audited and releases or rejects the held request. Timeout 60 s = deny. Prompts are rate-limited per plugin. Only users with the admin role may answer.

6. Publishing semantics

Publish(job_id, attempt_id, idempotency_key, account, post, checkpoint?) → outcome PUBLISHED | RETRYABLE_FAILURE | RATE_LIMITED | AUTH_FAILURE | PERMANENT_FAILURE | UNKNOWN + optional external_id/url, stable error_code, safe user-presentable error_message, retry_after. Plugins must be idempotent per attempt_id, honor the deadline, never log secrets. The Executioner decides retries/backoff/terminal states (default backoff 1 min → 5 min → 30 min → 2 h, max 5 attempts, configurable); plugins never schedule retries. Reconcile(job, checkpoint) → PUBLISHED | NOT_PUBLISHED | UNDECIDABLE. Never auto-retry after a timeout/crash/UNAVAILABLE that occurred after dispatch.

7. Errors and idempotency

Standard gRPC status + google.rpc.ErrorInfo (domain="postflowx", stable reason). No stack traces, SQL errors, secrets or paths to untrusted callers.

Status Reasons (examples) Retry
INVALID_ARGUMENT VALIDATION_ERROR no
UNAUTHENTICATED SESSION_INVALID, SENDER_UNKNOWN no
PERMISSION_DENIED FORBIDDEN, ROUTE_NOT_ALLOWED, CAPABILITY_NOT_GRANTED, APPROVAL_DENIED, APPROVAL_TIMEOUT, CALL_ID_INVALID no
NOT_FOUND absent or hidden no
ABORTED/FAILED_PRECONDITION CONFLICT, VERSION_MISMATCH, STATE_INVALID after re-read
RESOURCE_EXHAUSTED RATE_LIMITED, QUOTA_EXCEEDED later
DEADLINE_EXCEEDED may have completed query state first
UNAVAILABLE NOT_READY, DEPENDENCY_DOWN reads yes; mutations: query state first
UNIMPLEMENTED UNSUPPORTED_PROTOCOL no
INTERNAL no details no

Mutations where a duplicate would do harm take an idempotency key; the Gateway stores (sender, key) → result for a bounded time. A timed-out request may have completed — query before repeating. The Gateway never silently replays non-idempotent calls.

8. Plugin package and manifest

<id>/<version>/  main.json · code/<prebuilt executable> · assets/icon.svg (optional) · sha256sums (written by PMP)
{ "manifest_version": 1, "id": "com.postflowx.instagram", "name": "Instagram", "version": "0.1.0",
  "provides": ["publisher"], "api_compatibility": { "min": "1.0", "max_exclusive": "2.0" },
  "entrypoint": { "executable": "code/postflowx-plugin-instagram" },
  "capabilities_requested": ["storage.read","storage.write","accounts.manage","credentials.store","credentials.lease","media.read","media.public_url","audit.append"],
  "callbacks": ["oauth"],
  "tasks": [{ "name": "refresh_account", "scope": "account", "interval": "24h" }],
  "network": { "egress_hosts": ["graph.facebook.com", "graph.instagram.com"] },
  "resources": { "memory_mb": 256, "max_processes": 16 } }
  • The manifest is input, not a security boundary. PMP validates strictly (reverse-DNS lowercase ID, version syntax, known capabilities, task definitions, sizes; unknown fields rejected). Requested capabilities are only requests — the admin grants (reviewable, revocable, audited). Unknown capability = fail closed.
  • egress_hosts is informational in v1 (not enforced: shared network namespace). resources applied as rlimits best-effort. tasks are invoked by the Executioner with the account owner's context; plugins register no timers.
  • Capabilities (v1): storage.read/write, accounts.manage, credentials.store, credentials.lease, media.read, media.public_url, audit.append. No "all access", no DB capability. Kind publisher only; later ai_text.
  • Open (N-6): this assumes code/ contains a prebuilt executable. If the server should compile plugins from source, PMP and the server image change (see DECISIONS §3).

9. Plugin runtime rules

  • Isolation (Executioner applies, Controller granted the means): own UID/GID, no capabilities, no_new_privs, non-dumpable, clean env, private tmpfs, rlimits (NPROC, NOFILE, memory, CORE=0), private channel only, read-only plugin dir. Verified after every spawn (/proc/<pid>/status). Not provided in v1: egress filtering, protection from a malicious plugin with legitimately granted capabilities.
  • Trust levels: trusted (requests pass if granted) and untrusted (every request needs approval). Set by the admin locally; hash pinning at install detects tampering. Grants are always explicit.
  • Declarative UI: Describe returns name, icon (SVG sanitized, size-limited), connect flow (OAUTH2_REDIRECT | FORM | NONE), settings schema, post options schema, media rules, caption limit, API version. Schemas = restricted JSON-Schema subset (string with enum/maxLength/pattern, integer, number, boolean, arrays of those, nested object depth ≤ 2), validated at registration. Frontend owns the composer; the Gateway validates the final post against the schema, the plugin adds ValidatePost.
  • Data: only via the storage API; uninstall requires explicit retain/delete.
  • Lifecycle states: DISCOVERED → VALIDATING → INSTALLED → ENABLED → STARTING → READY; failures INVALID, INCOMPATIBLE, START_FAILED, UNHEALTHY, CRASH_LOOP, DISABLED, UNINSTALL_PENDING.
  • Updates (v1): operator installs a new version with the CLI; admin switches; capability changes need renewed approval. No remote auto-install.
  • SDK (postflowx-server/sdk/, Step 12d): generated stubs, service skeleton, call-context handling, Host API client, health service; demo plugin with failure injection; manifest validator.

10. Contract tests (add as you build)

Valid call; no/invalid cert; wrong sender for a route; forged identity fields; oversized message; expired deadline; unsupported version; unknown/expired/foreign call_id; plugin calling a route it has no channel to; untrusted request without approval / with deny / with timeout; raw SQL attempt; malformed bytes.