Skip to content

Lock agent-plugin strategy: Ghost as trust runtime + HTTP/MCP surface - #371

Merged
mohabbis merged 1 commit into
masterfrom
cursor/agent-plugin-runtime-dbda
Jul 28, 2026
Merged

Lock agent-plugin strategy: Ghost as trust runtime + HTTP/MCP surface#371
mohabbis merged 1 commit into
masterfrom
cursor/agent-plugin-runtime-dbda

Conversation

@mohabbis

Copy link
Copy Markdown
Owner

Strategy (locked)

Ghost is not another AI agent. It is the governed execution / trust runtime agents plug into:

Agent (propose) → Ghost (approve · execute · verify · audit)

Claude/Cursor/Codex may list, preview, and start runs. Humans approve sensitive steps in the Ghost UI. Working name may change later; strategy does not wait on naming.

What shipped

  • Docs / marketingdocs/product-direction.md, business-model, audiences, automation-strategy, AGENTS.md, cloud/docs/AGENT_PLUGIN.md, site hero copy.
  • @ghost/core/agent — allow-listed tools + explicit forbid list (approve_run, etc.).
  • /api/agent/* — catalog, invoke, workflows/runs/approvals mirrors. Auth: session or bearer GHOST_AGENT_API_KEY (env-bound org/user for local MCP).
  • cloud/apps/mcp — stdio JSON-RPC MCP bridge → HTTP invoke. No approve tools.
  • Hard rulePOST /api/agent/approvals and forbidden invoke names → 403.

Setup

See cloud/docs/AGENT_PLUGIN.md and cloud/.env.example (GHOST_AGENT_API_KEY, GHOST_AGENT_ORG_ID, GHOST_AGENT_USER_ID).

Validation

  • pnpm typecheck
  • pnpm test ✅ (core + mcp + worker; Postgres worker E2E included)
  • pnpm build

Follow-ups (not in this PR)

  • Per-org API keys in Postgres (replace env-bound identity for multi-tenant SaaS)
  • Settings UI to copy org/user id + generate key
  • Typed step editor / recording (existing Phase 1–2 backlog)

Trust

Agents cannot approve. Sensitive steps still halt via classifyStep → Ghost UI.

Open in Web Open in Cursor 

Position Ghost as the trust runtime AI agents plug into (propose → human
approve → execute → verify → audit). Add @ghost/core agent tool catalog,
/api/agent HTTP surface (session or API key; no self-approve), stdio MCP
bridge, and docs/marketing lock-in.

Co-authored-by: Muhammad Rafiq <mohabbis@users.noreply.github.com>
@vercel

vercel Bot commented Jul 28, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
ghost Ready Ready Preview, Comment Jul 28, 2026 6:08pm

@mohabbis
mohabbis marked this pull request as ready for review July 28, 2026 18:09

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 19d3b3fb40

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +59 to +65
const session = await auth();
if (!session?.user?.orgId || !session.user.id) {
return { ok: false, status: 401, error: "unauthorized" };
}
return {
ok: true,
principal: { orgId: session.user.orgId, userId: session.user.id, via: "session" },

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Separate agent sessions from human approval credentials

When an agent uses the supported session-cookie path, it has the same dashboard credential accepted by app/api/runs/[id]/approvals/[stepIndex]/route.ts, whose POST handler approves and re-enqueues a run using only auth(). The agent can therefore bypass the 403 agent endpoint by posting directly to that existing route, so omitting approval tools does not enforce the human-only gate. Require an agent-specific credential for this surface, or require separate human proof when resolving approvals.

AGENTS.md reference: AGENTS.md:L24-L26

Useful? React with 👍 / 👎.

Comment on lines +86 to +88
latestVersion: latest
? { version: latest.version, note: latest.note, steps: latest.steps }
: null,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Redact sensitive values from workflow previews

For workflows containing a fill step with sensitive: true, returning latest.steps verbatim exposes the stored password, OTP, card value, or other private input to the HTTP agent and causes the MCP bridge to serialize it into model context. A workflow preview should redact these values or expose only a placeholder under an explicitly sensitive-read permission rather than treating the full plan as safe metadata.

AGENTS.md reference: AGENTS.md:L123-L124

Useful? React with 👍 / 👎.

Comment on lines +12 to +13
"build": "tsup src/index.ts --format esm --target node20 --clean",
"start": "node dist/index.js",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Bundle the core catalog into the MCP build

After pnpm --filter @ghost/mcp build, this tsup invocation leaves a bare import of @ghost/core/agent in dist/index.js, while that export resolves to packages/core/src/agent/tools.ts. Running the declared node dist/index.js start command on Node 20 therefore exits with ERR_UNKNOWN_FILE_EXTENSION before MCP initializes; bundle @ghost/core or emit consumable JavaScript for the core package.

AGENTS.md reference: AGENTS.md:L47-L51

Useful? React with 👍 / 👎.

Comment on lines +106 to +110
const run = await prisma.run.create({
data: {
orgId: principal.orgId,
workflowVersionId: version.id,
triggeredById: principal.userId,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve the agent trigger identity in run audits

Every bearer-key start stores only the configured human userId, discarding principal.via; the worker later writes run.started and all step audit events using that same user as the actor. Consequently the audit trail cannot distinguish a dashboard action from an AI-agent/API-key action, so all agent-started runs are systematically misattributed. Persist the trigger channel or credential identity and include it in the audit metadata.

AGENTS.md reference: AGENTS.md:L24-L26

Useful? React with 👍 / 👎.

},
});

await enqueueRunWorkflow({ runId: run.id, orgId: principal.orgId });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Clean up runs when queue submission fails

In the agent start path, the Run is committed before BullMQ confirms the enqueue. If Redis is unavailable, invokeAgentTool returns 500 but leaves a permanently QUEUED run; if the enqueue succeeds remotely but its acknowledgment is lost, an agent retry can create and enqueue a second execution. Use an outbox/idempotency key or mark/delete the run when queue submission fails so callers can retry safely.

Useful? React with 👍 / 👎.

Comment on lines +135 to +142
let msg: JsonRpcRequest;
try {
msg = JSON.parse(trimmed) as JsonRpcRequest;
} catch {
process.stdout.write(fail(null, -32700, "parse error") + "\n");
continue;
}
const out = await handle(client, msg);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject non-object JSON-RPC messages without exiting

A syntactically valid JSON line such as null passes this parse and is sent to handle, which immediately destructures it and throws; the top-level catch then terminates the MCP process. A malformed client request should receive JSON-RPC -32600 while the server remains available, so validate that the parsed value is a non-array object before dispatch.

Useful? React with 👍 / 👎.

"ghost-mcp": "./dist/index.js"
},
"scripts": {
"dev": "tsx src/index.ts",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Exclude optional MCP from the default dev graph

Adding this dev task makes the repository-level turbo run dev launch @ghost/mcp alongside web and worker. With the agent plugin's optional key unset—as in the normal .env.example setup—the MCP process immediately exits with GHOST_AGENT_API_KEY is required, and Turbo terminates the web and worker tasks, so the documented pnpm dev command no longer starts Ghost Cloud unless an optional integration is configured. Launch MCP only through an explicit filtered command or keep it out of the default dev graph.

Useful? React with 👍 / 👎.

4. Add to `cloud/.env`:

```bash
GHOST_AGENT_API_KEY="$(openssl rand -hex 32)"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Generate the API key before writing the dotenv file

When this line is copied into cloud/.env as instructed, Next's dotenv loader does not execute the command substitution; it sets the API key to the literal, publicly known string $(openssl rand -hex 32). This either leaves a reachable dev server protected by a predictable credential or causes a mismatch when the MCP client uses the actual generated output. Instruct users to run openssl rand -hex 32 separately and paste its output into the dotenv assignment.

Useful? React with 👍 / 👎.

status: s.status,
verification: s.verification,
error: s.error,
screenshotUrl: s.screenshotKey ? `/api/artifacts/${s.screenshotKey}` : null,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Authorize bearer clients for returned screenshot URLs

For the documented MCP/API-key path, get_run returns this artifact URL but app/api/artifacts/[...key]/route.ts authenticates exclusively with auth() and rejects the same bearer key with 401. The agent therefore receives an unusable screenshot reference for every captured step. Resolve the agent principal while preserving the existing organization ownership check, or return a scoped presigned URL suitable for the bearer client.

AGENTS.md reference: AGENTS.md:L124-L124

Useful? React with 👍 / 👎.

Comment on lines +10 to +12
Claude, Cursor, Codex, ChatGPT, and similar tools may **list workflows, preview
plans, start runs, and observe status**. They **must not approve** sensitive
steps — that stays human-only in the Ghost UI.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Remove the unsupported ChatGPT availability claim

The shipped surface described below consists of local stdio MCP plus the early bearer HTTP API, while this same document explicitly lists shipping ChatGPT remote MCP/OAuth as a non-goal. Naming ChatGPT among clients that can currently list and start runs therefore promises an integration path the repository does not provide. Qualify ChatGPT as future work until a verified remote transport and authentication flow exist.

AGENTS.md reference: AGENTS.md:L96-L97

Useful? React with 👍 / 👎.

@mohabbis
mohabbis merged commit daa6242 into master Jul 28, 2026
24 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants