Serverless script orchestration platform built on Cloudflare Workers. Deploy PowerShell and Bash scripts to any number of remote machines — no VPN, no open ports, no agent service required.
- Features
- Why Edgex?
- Why Cloudflare Workers?
- How it works
- Architecture
- Security
- Stack
- Project structure
- Database schema
- API reference
- Configuration
- Setup
- Local development
- PowerShell & Bash support — upload, version, and manage scripts for both Windows and Linux/macOS targets
- In-browser editor — syntax-highlighted script editor directly in the dashboard, no external tooling needed
- Automatic versioning — every update bumps the patch version (
1.0.0 → 1.0.1); full version history visible in the UI - R2-backed storage — scripts are stored durably in Cloudflare R2 with zero egress fees
- One-liner bootstrapper — a single PowerShell or Bash command enrolls a machine and executes the script; nothing is left behind
- Multi-machine deployments — deploy to any number of machines simultaneously; track each one individually
- Time-limited or permanent deployments — deployments can expire after a configurable number of hours or run indefinitely (
expires_hours: null) - Re-run protection — configurable per-platform limit on how many times a single machine can re-run the same deployment; enforced atomically (no race conditions)
- Live status tracking — per-machine status (
running,completed,failed,expired) updated in real time as the script executes - Structured feedback — agents report back
info,success,warning, anderrorlog entries with optional step names; all stored in D1 and visible in the dashboard - Cancel at any level — cancel an entire deployment or a single machine target with one click
- Zero Trust auth — admin dashboard and all admin API routes protected by Cloudflare Access (JWKS + Web Crypto JWT verification in-process)
- TOFU enrollment — Trust On First Use: the first machine to enroll with a given
machine_idgets a unique cryptographic token; subsequent enrollments are idempotent; hostname conflicts are rejected - Cryptographic Kill Switch — SHA-256 checksum of the script is pinned at deployment creation; a single-bit difference in the stored file instantly expires the deployment and blocks all machines from downloading it
- Token isolation — the
master_token(shared per deployment) is used only for enrollment; all subsequent agent calls use a per-machineagent_tokenthat cannot be used to enroll new machines - Wrapper snapshot isolation — the PS1/Bash wrapper template active at deployment creation is snapshotted in D1; later admin edits to the wrapper do not affect running deployments
- Script generation — describe what a script should do in plain language; Edgex generates production-ready PowerShell or Bash using
@cf/qwen/qwen2.5-coder-32b-instructrunning on Workers AI (on-platform, no external API) - Refactoring — paste existing code and ask for improvements; the model rewrites it while preserving intent
- Security audit — one-click audit flags potential vulnerabilities, unsafe patterns, and privilege escalation risks in your scripts
- Explain mode — get a plain-English breakdown of what any script does, line by line
- Enterprise glassmorphism dashboard — dark background, frosted-glass cards, served at
/dashboard/ - Live geo map — every enrolled machine appears on an interactive Leaflet map (CartoDB Dark Matter tiles) with hub-and-spoke animated polylines showing real-time connectivity
- Geo metadata —
colo,city,country,latitude,longitudecaptured automatically from each agent request via Cloudflare edge metadata; no GeoIP library needed - Bilingual UI — full English / Polish localisation (EN/PL), switchable at runtime without a page reload
- Customisable wrapper templates — edit the PS1 and Bash wrapper boilerplate (helper functions injected around every script) directly from the Settings panel; changes are logged with timestamps
Traditional RMM tools require either an always-on agent service (attack surface), an open inbound port (firewall nightmare), or a VPN tunnel (ops overhead). Edgex takes a different approach:
- Pull-based execution — machines call out to the platform, never the other way around. No inbound ports needed.
- Ephemeral scripts — scripts are not stored on the machine. They are downloaded, executed in memory, and discarded.
- Zero persistent agent — the bootstrapper is a one-liner. No service to install, no registry entries, no scheduled tasks left behind.
- Per-machine identity — every machine gets a unique cryptographic token at enrollment. A leaked token compromises one machine, not the entire deployment.
- Cryptographic integrity — the platform pins a SHA-256 checksum of the script at deployment creation time. If the stored script is tampered with at any point, the deployment is killed instantly before any machine receives the compromised payload.
- Bilingual UI — the admin dashboard is fully localised in English and Polish (EN/PL), switchable at runtime without a page reload.
| Property | Benefit |
|---|---|
| Edge network (300+ PoPs) | Scripts are served from the closest datacenter to the target machine. Latency is minimal regardless of geography. |
| No cold starts | Workers run in V8 isolates, not containers. Startup time is ~1 ms, not seconds. |
| No infrastructure to manage | No EC2, no load balancer, no TLS termination, no autoscaling groups. |
| D1 (SQLite at the edge) | Relational database with full SQL, zero ops. |
| R2 (object storage) | Script files stored durably with zero egress fees. |
| Workers AI | On-platform LLM for script generation, refactoring and security auditing — no external API calls. |
| Cloudflare Access | Zero-trust authentication for the admin dashboard — cryptographic JWT verification, no passwords to manage. |
| Built-in geo metadata | Every agent request carries colo, city, country, latitude, longitude from the CF edge — no GeoIP library needed. |
Edgex separates concerns into two distinct token-based flows:
sequenceDiagram
participant Admin
participant Dashboard
participant Worker
participant D1
participant R2
Admin->>Dashboard: Upload script
Dashboard->>Worker: POST /v1/admin/scripts
Worker->>R2: Store script file
Worker->>D1: INSERT Scripts (checksum, version)
Admin->>Dashboard: Create deployment
Dashboard->>Worker: POST /v1/admin/deployments
Worker->>D1: Fetch script checksum + wrapper template
Worker->>D1: INSERT Deployments (master_token, script_checksum, wrapper_snapshot)
Worker-->>Dashboard: master_token + installer one-liner
sequenceDiagram
participant Machine
participant Worker
participant D1
participant R2
Note over Machine: Admin pastes one-liner on target
Machine->>Worker: GET /v1/agent/installer (Bearer: master_token)
Worker-->>Machine: Bootstrapper script (PS1 or Bash)
Machine->>Worker: POST /v1/agent/enroll (hostname, machine_id)
Note over Worker: TOFU — first seen machine_id → new target
Worker->>D1: INSERT DeploymentTargets (agent_token)
Worker-->>Machine: agent_token (unique per machine)
Machine->>Worker: GET /v1/agent/download (Bearer: agent_token)
Worker->>R2: Fetch script content
Note over Worker: SHA-256 verify vs. pinned checksum
Worker->>D1: Inject agent_token into wrapper
Worker-->>Machine: Wrapped + encoded script
loop Execution
Machine->>Worker: POST /v1/agent/feedback (status, message, step)
Worker->>D1: INSERT Logs, UPDATE target status
end
stateDiagram-v2
[*] --> master_token: Deployment created
master_token --> agent_token: Machine enrolls (TOFU)
master_token --> [*]: Deployment expires / cancelled
agent_token --> running: /download called
running --> completed: feedback step=complete status=success
running --> failed: feedback status=error
running --> expired: admin cancel
completed --> running: re-run (if under limit)
failed --> running: re-run (if under limit)
graph TB
subgraph Admin["Admin (browser)"]
UI["Dashboard UI\n/dashboard/*"]
end
subgraph CF["Cloudflare Edge"]
Access["Cloudflare Access\nJWT verification"]
Worker["Edgex Worker\nHono on Workers Runtime"]
D1[("D1 Database\nSQLite")]
R2[("R2 Bucket\nScript files")]
AI["Workers AI\nQwen 2.5 Coder 32B"]
end
subgraph Machines["Target machines"]
PS["Windows\nPowerShell 5.1+"]
Bash["Linux / macOS\nBash"]
end
UI -->|HTTPS + CF Access JWT| Access
Access --> Worker
Worker --- D1
Worker --- R2
Worker --- AI
PS -->|Bearer agent_token| Worker
Bash -->|Bearer agent_token| Worker
The script is never transmitted in plain text to the machine. It is:
- Base64-encoded server-side before injection into the wrapper template.
- Decoded in memory on the machine at runtime — no file written to disk.
- Executed inside an isolated scope (PowerShell
[scriptblock]::Create()dot-sourced; Bashevalinside a subshell). - Discarded after execution — nothing persists on the machine.
graph LR
Raw["Raw script\n(R2)"] -->|SHA-256 verify| Check{"Checksum\nmatch?"}
Check -->|No| Kill["Kill Switch:\nexpire deployment\nlog violation\nreturn 500"]
Check -->|Yes| Encode["Base64 encode payload"]
Encode --> Template["Inject into\nwrapper template"]
Template --> Send["Serve to machine\nCache-Control: no-store"]
Send --> Exec["Machine decodes\n+ executes in memory"]
| Layer | Mechanism | Protects |
|---|---|---|
| Cloudflare Access | JWKS-verified JWT (Cf-Access-Jwt-Assertion) |
All admin routes + dashboard UI |
deploymentAuth |
Bearer master_token (shared per deployment) |
/installer, /enroll only |
agentAuth |
Bearer agent_token (unique per machine) |
/download, /feedback |
Cloudflare Access is verified in-process inside the Worker (src/middleware/accessAuth.ts): the JWT signature is validated against the team's JWKS endpoint using Web Crypto, and the aud + exp claims are checked. This is defense-in-depth — even if the edge-level CF Access policy were bypassed, the Worker itself rejects unsigned requests.
Important: Every Cloudflare Worker gets a public
*.workers.devURL that is not covered by your custom-domain CF Access policies by default. Anyone who knows theworkers.devURL can reach your Worker without going through the Access-protected custom domain.
Mitigate this by doing one of the following (both recommended):
- Disable
workers.dev— go to Workers & Pages → scripter → Settings → Domains & Routes and toggle workers.dev Route off. - Add a separate CF Access policy for the
workers.devdomain covering/dashboard/*and/v1/admin/*.
Agent routes (/v1/agent/*) rely on Bearer tokens, not CF Access, so they are unaffected.
When WORKER_URL starts with http://localhost, the Worker skips JWKS verification entirely and injects dev@local.host as the authenticated user. This makes wrangler dev work without a real CF Access setup.
Never set
WORKER_URL=http://localhost(or any non-HTTPS value) in a deployed environment — it disables all JWT authentication for admin routes.
Zero Trust Storage (Cryptographic Kill Switch)
At deployment creation, the SHA-256 checksum of the script is pinned in D1. On every /download request, the Worker recomputes the hash from the R2 object and compares it to the pinned value. A single-bit difference (tampering, corruption, supply-chain attack) triggers:
- Immediate expiry of the entire deployment
kill_reason = 'INTEGRITY_VIOLATION'written to D1- Generic
500returned to the agent — no hash details leaked
TOFU enrollment (Trust On First Use)
The first machine to enroll with a given machine_id gets a unique agent_token. Subsequent enrollments with the same machine_id return the same token (idempotent). A different machine claiming the same hostname is rejected with 409 HOSTNAME_CONFLICT.
Token isolation
master_token is used only for enrollment. Once a machine has its agent_token, the master_token is never used again by that machine. Leaking the master token allows enrollment of new machines, but does not grant access to any existing machine's execution context.
Enrollment cap
POST /v1/agent/enroll checks enrolled_count >= expected_machines before creating a new target. Prevents unbounded target creation even with a leaked master token.
Wrapper snapshot isolation The wrapper template active at deployment creation time is snapshotted into D1. Subsequent admin edits to the wrapper do not affect running deployments — each deployment executes with exactly the wrapper that was in effect when it was created.
Re-run TOCTOU protection
Re-runs use an atomic UPDATE … WHERE run_count < maxRuns. If two concurrent requests race, exactly one succeeds — the other gets 429 RERUN_LIMIT_EXCEEDED. No race condition.
Input validation
| Field | Constraint |
|---|---|
hostname |
≤ 253 chars, no CRLF / null bytes |
machine_id |
≤ 300 chars, no CRLF / null bytes |
step |
≤ 100 chars |
message |
≤ 4 096 chars |
script content |
≤ 1 MiB |
prompt (AI) |
≤ 8 000 chars |
Route :id params |
Parsed with parseInt — NaN / non-positive → 400 |
HTTP security headers (applied to all responses):
Strict-Transport-Security: max-age=63072000; includeSubDomains
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=()
Content-Security-Policy: default-src 'none'; script-src 'self' 'unsafe-inline' https://cdn.tailwindcss.com https://unpkg.com https://static.cloudflareinsights.com; connect-src 'self' https://cloudflareinsights.com; …
Cache-Control: no-store (all JSON API responses)
| Component | Technology |
|---|---|
| Runtime | Cloudflare Workers (V8 isolates) |
| Framework | Hono v4.12.2 |
| Database | Cloudflare D1 (SQLite) |
| Object storage | Cloudflare R2 |
| AI | Workers AI — @cf/qwen/qwen2.5-coder-32b-instruct |
| Auth | Cloudflare Access (JWKS + Web Crypto JWT verification) |
| UI | Single-file SPA (Tailwind CDN, Leaflet, vanilla JS) |
| Tooling | Wrangler v4, TypeScript |
src/
├── index.ts # Entry point — routing, CSP, security headers
├── env.ts # Hono AppEnv / Bindings / Variables types
│
├── db/
│ ├── schema.sql # D1 schema (apply once per environment)
│ ├── types.ts # TypeScript interfaces for DB rows
│ └── migrations/
│ ├── 001_geo_fields.sql # Geo columns on DeploymentTargets
│ ├── 003_rerun.sql # run_count + SystemSettings
│ ├── 004_wrapper_templates.sql # WrapperTemplates + logs
│ ├── 005_enrollment.sql # agent_token + machine_id (TOFU)
│ ├── 006_integrity_snapshot.sql # script_checksum, wrapper_snapshot
│ ├── 007_kill_reason.sql # kill_reason on Deployments
│ └── 008_permanent_deployments.sql # expires_at nullable
│
├── middleware/
│ ├── accessAuth.ts # Cloudflare Access JWT verification
│ └── auth.ts # deploymentAuth, agentAuth, adminAuth
│
├── routes/
│ ├── agent.ts # /v1/agent/* — installer, enroll, download, feedback
│ ├── admin.ts # /v1/admin/* — scripts, deployments, AI, settings, wrappers
│ ├── deployments.ts # DEPRECATED — empty stub, do not import
│ └── feedback.ts # DEPRECATED — empty stub, do not import
│
├── ui/
│ └── dashboard_v2.ts # Dashboard SPA (glassmorphism, enterprise black theme)
│
└── lib/
├── wrapper.ts # buildWrapper(), default PS1 + Bash templates
└── ai-prompts.ts # System prompts for generate / refactor / audit / explain
erDiagram
Scripts {
int id PK
text name
text type "powershell | bash"
text r2_key
text version "semver"
text checksum "SHA-256"
text created_by_email
text created_at
}
Deployments {
int id PK
int script_id FK
text name
text master_token "UNIQUE — shared per deployment"
text expires_at "NULL = permanent"
int expected_machines
text script_checksum "pinned at creation"
text script_version "pinned at creation"
text wrapper_snapshot "pinned at creation"
text kill_reason "NULL or INTEGRITY_VIOLATION"
text created_by_email
text created_at
}
DeploymentTargets {
int id PK
int deployment_id FK
text hostname
text machine_id "hardware UUID"
text agent_token "UNIQUE per machine"
text status "running|completed|failed|expired"
int run_count
text colo
text city
text country
real latitude
real longitude
text started_at
text completed_at
text last_seen_at
}
Logs {
int id PK
int target_id FK
text step
text status "info|success|error|warning"
text message
text timestamp
}
SystemSettings {
text key PK
text value
}
WrapperTemplates {
text type PK "powershell | bash"
text content
text updated_at
text updated_by
}
Scripts ||--o{ Deployments : "deployed as"
Deployments ||--o{ DeploymentTargets : "runs on"
DeploymentTargets ||--o{ Logs : "emits"
| Method | Path | Auth | Description |
|---|---|---|---|
GET |
/v1/agent/installer?type=ps1|bash |
master_token | Returns bootstrapper script |
POST |
/v1/agent/enroll |
master_token | TOFU enrollment → agent_token |
GET |
/v1/agent/download |
agent_token | Returns wrapped + encoded script |
POST |
/v1/agent/feedback |
agent_token | Send execution log entry |
Enroll request body:
{ "hostname": "WORKSTATION-01", "machine_id": "a1b2c3d4-..." }Feedback request body:
{ "status": "info|success|error|warning", "message": "...", "step": "optional-step-name" }| Method | Path | Description |
|---|---|---|
GET |
/v1/admin/me |
Returns authenticated user email |
GET |
/v1/admin/scripts |
List all scripts |
POST |
/v1/admin/scripts |
Create script (upload to R2 + D1) |
GET |
/v1/admin/scripts/:id |
Script metadata by ID |
GET |
/v1/admin/scripts/:id/content |
Raw script content from R2 |
PUT |
/v1/admin/scripts/:id |
Update script (bumps patch version) |
DELETE |
/v1/admin/scripts/:id |
Delete script |
GET |
/v1/admin/deployments |
List deployments with target counts |
POST |
/v1/admin/deployments |
Create deployment |
GET |
/v1/admin/deployments/:id |
Deployment detail + targets |
GET |
/v1/admin/deployments/:id/installer |
Universal bootstrapper |
PUT |
/v1/admin/deployments/:id/cancel |
Cancel deployment + running targets |
DELETE |
/v1/admin/deployments/:id |
Hard delete deployment |
GET |
/v1/admin/deployments/:id/logs |
Recent logs feed (all targets) |
GET |
/v1/admin/deployments/:id/targets/:tid/logs |
Logs for one target |
PUT |
/v1/admin/deployments/:id/targets/:tid/cancel |
Cancel single target |
GET |
/v1/admin/settings |
Get platform settings |
PUT |
/v1/admin/settings |
Update platform settings |
POST |
/v1/admin/ai/process |
AI code generation / refactor / audit / explain |
GET |
/v1/admin/wrapper/:type |
Get wrapper template (ps1 or bash) |
PUT |
/v1/admin/wrapper/:type |
Update wrapper template |
DELETE |
/v1/admin/wrapper/:type |
Reset wrapper to default |
GET |
/v1/admin/wrapper/:type/logs |
Wrapper change log |
Create deployment body:
{
"script_id": 1,
"name": "Patch Tuesday — Floor 2",
"description": "optional",
"expected_machines": 10,
"expires_hours": 72
}Set expires_hours to 0 or null for a permanent deployment (never expires).
Copy wrangler.toml.example to wrangler.toml and fill in your values:
name = "edgex"
main = "src/index.ts"
compatibility_date = "2026-02-22"
[[routes]]
pattern = "edgex.your-domain.com"
zone_name = "your-domain.com"
custom_domain = true
[vars]
WORKER_URL = "https://edgex.your-domain.com"
[[d1_databases]]
binding = "DB"
database_name = "edgex-db"
database_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
[[r2_buckets]]
binding = "SCRIPTS_BUCKET"
bucket_name = "edgex-scripts"
[ai]
binding = "AI"Set via wrangler secret put:
wrangler secret put CF_ACCESS_AUD # Cloudflare Access application AUD tag
wrangler secret put CF_ACCESS_TEAM_DOMAIN # e.g. your-team.cloudflareaccess.comFind these values in your Cloudflare Zero Trust dashboard under Access → Applications.
- Cloudflare account
- Wrangler CLI (
npm i -g wrangler) - Node.js 18+
git clone https://github.com/your-username/edgex.git
cd edgex
npm install# D1 database
wrangler d1 create edgex-db
# R2 bucket
wrangler r2 bucket create edgex-scriptsPaste the returned database_id into wrangler.toml.
# Local development
wrangler d1 execute edgex-db --local --file=src/db/schema.sql
# Production
wrangler d1 execute edgex-db --file=src/db/schema.sql- Go to Zero Trust → Access → Applications → Add an application
- Choose Self-hosted, set the domain to match
WORKER_URL - Copy the AUD tag and your team domain
- Set them as secrets:
wrangler secret put CF_ACCESS_AUD
wrangler secret put CF_ACCESS_TEAM_DOMAINwrangler deploy# Reset local D1 state
rm -rf .wrangler/state/v3/d1/
# Apply schema locally
wrangler d1 execute edgex-db --local --file=src/db/schema.sql
# Start local dev server (CF Access is bypassed automatically for localhost)
wrangler devThe dashboard is available at http://localhost:8787/dashboard/.
In local dev mode, CF Access JWT verification is skipped and dev@local.host is used as the authenticated email. The Authorization header for agent endpoints still needs to be provided with a valid token from the local D1 database.
Scripts uploaded to Edgex have access to these helpers at runtime — they are injected by the wrapper and do not need to be imported or defined:
PowerShell
_Orc-Send -Status 'info'|'success'|'error'|'warning' -Message 'text' [-Step 'step-name']
Get-OrcSystemInfo # Returns hashtable: Hostname, OS, OSBuild, RAM_GB, IP, User, Domain, TimeZone, MachineId
Test-OrcAdmin # Returns $true if running as Administrator
Invoke-OrcWithRetry -ScriptBlock { ... } [-MaxAttempts 3] [-DelaySeconds 5]Bash
_orc_send 'info|success|error|warning' 'message' ['step-name']
_orc_system_info # Prints: Hostname | OS | Arch | RAM | IP | User
_orc_is_root # Returns exit code 0 if running as root
_orc_retry 'command' [max_attempts] [delay_sec] ['step-name']
# Read-only platform variables:
# $_ORC_OS linux | macos | freebsd | …
# $_ORC_OS_VERSION e.g. "Ubuntu 24.04.1 LTS"
# $_ORC_ARCH x86_64 | arm64 | …
# $_ORC_MACHINE_ID stable hardware UUID (or fallback:sha256)
# $_ORC_HOSTNAME hostnameMIT