Skip to content
Open
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
2 changes: 1 addition & 1 deletion SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -1156,7 +1156,7 @@ The UI adds convenience (visual builder, drag-and-drop, dashboards) but introduc

**Project name:** OpenSOP
**Domain:** opensop.ai
**Tagline:** "Define your processes. Get your API."
**Tagline:** "The open standard for executable processes."
**Repo:** `Chosen9115/opensop`
**License:** Apache 2.0 (permissive, enterprise-friendly, patent grant)

Expand Down
58 changes: 58 additions & 0 deletions app/jobs/demo/reset_job.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# frozen_string_literal: true

module Demo
# Nightly reset job for the public demo deployment.
#
# Runs on the schedule defined by `Opensop::DemoMode::RESET_SCHEDULE_CRON`
# (configured in config/recurring.yml). Clears all process instances so the
# demo always starts each day from a clean, seeded state.
#
# Behaviour:
# - No-op when DEMO_MODE is not enabled (safe to leave in recurring.yml
# on non-demo deployments — it just logs and exits).
# - In a single transaction: destroys all Sop::Instance rows, purges
# Sop::Process registrations, and calls Demo::SeedLoader.call to
# reload definitions cleanly from processes/examples/.
# - Logs a one-liner with cleared/reseeded counts so the job appears in
# Solid Queue's finished-job log and the Rails structured log.
class ResetJob < ApplicationJob
queue_as :default

def perform
unless Opensop::DemoMode.enabled?
Rails.logger.info("[demo-reset] skipped — DEMO_MODE not enabled")
return
end

cleared = 0
processes_cleared = 0
seed_result = nil

ActiveRecord::Base.transaction do
# Delete children in dependency order before removing instances.
# Sop::Instance declares `dependent: :destroy` for steps, events, and
# callbacks, but we use bulk deletes here for speed since this is a
# full reset — referential safety is guaranteed by the explicit ordering.
Sop::Callback.delete_all
Sop::Event.delete_all
Sop::Step.delete_all
cleared = Sop::Instance.delete_all

# Purge process registrations too — without this, stale entries from
# prior mis-seedings survive every reset. Demo::SeedLoader upserts
# by name rather than replacing, so a Sop::Process row registered by
# a previous (broader) seed glob stays in the table forever even after
# the underlying YAML file moves out of the seeded path. Delete-all
# here guarantees SeedLoader.call below reseeds from a clean slate.
processes_cleared = Sop::Process.delete_all

seed_result = Demo::SeedLoader.call
end

Rails.logger.info(
"[demo-reset] cleared #{cleared} instances, #{processes_cleared} stale processes; " \
"reseeded #{seed_result.processes_loaded} processes"
)
end
end
end
64 changes: 64 additions & 0 deletions app/services/demo/seed_loader.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# frozen_string_literal: true

module Demo
# Idempotent seed loader for the public demo deployment.
#
# Loads all example process definitions from `processes/examples/` and
# confirms the demo API token is configured, logging a clear message for
# operators. Designed to run safely in two contexts:
#
# 1. `db:seed` — via the hook at the bottom of db/seeds.rb
# 2. `Demo::ResetJob` — as part of the nightly 3:00 UTC data reset
#
# No-op when `Opensop::DemoMode.enabled?` is false, so this is safe to
# deploy in non-demo environments without setting any extra env vars.
#
# Returns a Result struct with :processes_loaded and :token_configured so
# callers and specs can assert on outcomes without parsing log output.
#
# Note on ApiToken: OpenSOP authenticates /sop/* requests via the
# `OPENSOP_API_TOKEN` environment variable (see Sop::ApplicationController).
# There is no ApiToken ActiveRecord model. The demo token is wired by
# setting OPENSOP_API_TOKEN=<Opensop::DemoMode.api_token> in the deployment
# environment. This loader validates that the env var matches and logs a
# clear warning when it does not, so operators catch mismatches early.
module SeedLoader
Result = Struct.new(:processes_loaded, :token_configured, keyword_init: true)

module_function

def call
unless Opensop::DemoMode.enabled?
Rails.logger.info("[demo-seed] skipped — DEMO_MODE not enabled")
return Result.new(processes_loaded: 0, token_configured: false)
end

# Confirm the API token env var is set and matches the expected demo token.
# Auth is env-based; this is a configuration check, not a DB write.
expected_token = Opensop::DemoMode.api_token
actual_token = ENV.fetch("OPENSOP_API_TOKEN", "").strip
token_ok = actual_token == expected_token

unless token_ok
Rails.logger.warn(
"[demo-seed] OPENSOP_API_TOKEN does not match DEMO_API_TOKEN. " \
"Set OPENSOP_API_TOKEN=#{expected_token} (or set DEMO_API_TOKEN to match OPENSOP_API_TOKEN)."
)
end

# Load process definitions scoped to processes/examples/ ONLY.
# Passing examples_path as root means Registry.load_all globs
# processes/examples/**/*.sop.yaml — private subdirectories such as
# processes/coba/ are never touched.
examples_path = Rails.root.join("processes", "examples")
loaded = Opensop::Registry.load_all(examples_path)

Rails.logger.info(
"[demo-seed] loaded #{loaded.size} processes, " \
"demo token #{token_ok ? 'ready' : 'MISMATCH — check env vars'}"
)

Result.new(processes_loaded: loaded.size, token_configured: token_ok)
end
end
end
2 changes: 1 addition & 1 deletion config/locales/opensop.en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ en:
opensop:
app:
name: "OpenSOP"
tagline: "Define your processes. Get your API."
tagline: "The open standard for executable processes."

auth:
sign_in:
Expand Down
21 changes: 16 additions & 5 deletions db/seeds.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,20 @@
# development, test). The code here should be idempotent so that it can be executed at any point in every environment.
# The data can then be loaded with the bin/rails db:seed command (or created alongside the database with db:setup).

# Load all OpenSOP process definitions from the `processes/` directory.
loaded = Opensop::Registry.load_all
puts "[seed] loaded #{loaded.size} OpenSOP process definition(s)"
loaded.each do |record|
puts "[seed] - #{record.name} v#{record.version} (status: #{record.status})"
if Opensop::DemoMode.enabled?
# Demo deployment: load ONLY the curated examples from processes/examples/.
# Do NOT use the broad Registry.load_all glob here — that would pick up
# private subdirectories (processes/coba/, etc.) and pollute the public
# demo catalog. Demo::SeedLoader is scoped to processes/examples/ and also
# validates the demo API token.
result = Demo::SeedLoader.call
puts "[seed] demo mode: #{result.processes_loaded} example process(es) ready, token_configured=#{result.token_configured}"
else
# Standard deployment: load all process definitions from processes/**/*.sop.yaml.
# Forks drop their private processes/ subdirectory here and they auto-load.
loaded = Opensop::Registry.load_all
puts "[seed] loaded #{loaded.size} OpenSOP process definition(s)"
loaded.each do |record|
puts "[seed] - #{record.name} v#{record.version} (status: #{record.status})"
end
end
30 changes: 30 additions & 0 deletions lib/opensop/demo_mode.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# frozen_string_literal: true

module Opensop
module DemoMode
module_function

# True when DEMO_MODE env var is set to "true" (case-insensitive, trimmed).
# Anything else (unset, "false", "0", empty, "True " — yes trimmed) is false.
def enabled?
ENV.fetch("DEMO_MODE", "").to_s.strip.downcase == "true"
end

# The public API token announced on the demo homepage.
#
# Prefers OPENSOP_API_TOKEN — the same env var Sop::ApplicationController
# uses for /sop/* auth — so a single secret powers both auth and the
# value displayed to visitors. Falls back to DEMO_API_TOKEN (kept for
# local dev convenience) and finally a stable default so dev never crashes.
def api_token
ENV.fetch("OPENSOP_API_TOKEN") do
ENV.fetch("DEMO_API_TOKEN", "demo-public-token-resets-daily")
end
end

# The default reset cron schedule, exposed as a single constant so the
# banner UI and the recurring job spec can reference it without drift.
RESET_SCHEDULE_CRON = "0 3 * * *"
RESET_SCHEDULE_HUMAN = "daily at 3:00 UTC"
end
end
177 changes: 177 additions & 0 deletions processes/examples/agent-pr-review.sop.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
# Demo: Agent PR Review
# Step types demonstrated: form, judgment, automated, notification
# Status in v0.1: mixed (form + automated fully executable; judgment is stubbed — state transitions only)
#
# AGENT HARNESS PATTERN: This process demonstrates how OpenSOP wraps an LLM
# code-review call inside a typed, auditable gate. The `judgment` step receives
# the PR diff and metadata as structured inputs, emits a typed decision
# (approve | request_changes | comment), and records confidence + decided_by.
# Humans can always override via POST /sop/agent-pr-review/{id}/steps/review-pr/submit.
#
# GITHUB INTEGRATION NOTE: Real GitHub comment posting (the `post-comment` step)
# is out of scope for v0.1. The step runs ./steps/post-pr-comment.rb which
# simulates a successful post to stdout without making any real API calls.
# Wire a real GitHub token and API call when hardening for production.

opensop: "0.1"

process:
name: agent-pr-review
version: "1.0"
description: "An agent reviews a pull request diff and emits a typed decision (approve, request_changes, or comment); a stub posts the review; the author is notified."
owner: platform-team

trigger:
type: api

inputs:
- name: pr_url
type: string
required: true
description: "Full GitHub PR URL (e.g. https://github.com/org/repo/pull/42)"
- name: repo
type: string
required: true
description: "Repository slug (e.g. acme/api-gateway)"
- name: base_branch
type: string
required: true
description: "Target branch the PR merges into (e.g. main)"
- name: author_email
type: string
format: email
required: true
description: "PR author email for notification"
- name: reviewer_context
type: string
description: "Optional instructions or focus areas for the review agent"

outputs:
- name: review_decision
type: string
from: steps.review-pr.outputs.review_decision
- name: summary
type: string
from: steps.review-pr.outputs.summary
- name: comment_url
type: string
from: steps.post-comment.outputs.comment_url

steps:
- id: collect-pr-details
name: "Collect PR details and diff"
type: form
description: "Agent or engineer provides the diff and metadata needed for review"
inputs:
- name: pr_url
from: process.inputs.pr_url
- name: repo
from: process.inputs.repo
- name: base_branch
from: process.inputs.base_branch
outputs:
- name: title
type: string
- name: description
type: string
- name: diff
type: string
description: "Full unified diff of the PR (git diff output)"
- name: files_changed
type: number
- name: lines_added
type: number
- name: lines_removed
type: number
timeout: 30m
on_timeout: notify-and-wait

- id: review-pr
name: "Agent code review"
type: judgment
description: "LLM reviews the PR diff for correctness, style, security issues, and test coverage; emits a typed decision with line-level comments"
inputs:
- name: pr_url
from: process.inputs.pr_url
- name: repo
from: process.inputs.repo
- name: base_branch
from: process.inputs.base_branch
- name: title
from: steps.collect-pr-details.outputs.title
- name: description
from: steps.collect-pr-details.outputs.description
- name: diff
from: steps.collect-pr-details.outputs.diff
- name: files_changed
from: steps.collect-pr-details.outputs.files_changed
- name: reviewer_context
from: process.inputs.reviewer_context
outputs:
- name: review_decision
type: enum
values: [approve, request_changes, comment]
- name: summary
type: string
description: "High-level review summary (1-3 paragraphs)"
- name: line_comments
type: string[]
description: "Array of file:line:comment strings for specific feedback"
- name: security_flags
type: string[]
description: "Any security concerns identified"
- name: test_coverage_ok
type: boolean
judgment:
allow_agent: true
require_human_review: false
confidence_threshold: 0.80
escalation: manual

- id: post-comment
name: "Post review comment (demo stub)"
type: automated
description: "Simulates posting the review to GitHub. In production, replace this script with a real GitHub API call. See NOTE in file header."
inputs:
- name: pr_url
from: process.inputs.pr_url
- name: review_decision
from: steps.review-pr.outputs.review_decision
- name: summary
from: steps.review-pr.outputs.summary
- name: line_comments
from: steps.review-pr.outputs.line_comments
outputs:
- name: comment_posted
type: boolean
- name: comment_url
type: string
run: ./examples/steps/post-pr-comment.rb
retry:
max: 2
backoff: exponential

- id: notify-author
name: "Notify PR author"
type: notification
inputs:
- name: author_email
from: process.inputs.author_email
- name: pr_url
from: process.inputs.pr_url
- name: review_decision
from: steps.review-pr.outputs.review_decision
- name: comment_url
from: steps.post-comment.outputs.comment_url
- name: summary
from: steps.review-pr.outputs.summary

on_error:
notify:
channel: slack
target: "#platform-alerts"

tags: [developer-tooling, code-review, agent-harness, ai]
sla:
target: 30m
warning: 15m
Loading
Loading