Skip to content
Merged
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 .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "deriv",
"displayName": "Deriv",
"version": "1.0.1",
"version": "1.0.2",
"description": "Live Deriv API schemas, payload validation, and guides for authentication, subscriptions, trades, and errors.",
"author": { "name": "Deriv", "email": "api-support@deriv.com" },
"homepage": "https://developers.deriv.com",
Expand Down
2 changes: 1 addition & 1 deletion .codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "deriv",
"version": "1.0.1",
"version": "1.0.2",
"description": "Live Deriv API schemas, payload validation, and guides for authentication, subscriptions, trades, and errors.",
"author": {
"name": "deriv",
Expand Down
2 changes: 1 addition & 1 deletion .cursor-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "deriv",
"displayName": "Deriv",
"version": "1.0.1",
"version": "1.0.2",
"description": "Live Deriv API schemas, payload validation, and guides for authentication, subscriptions, trades, and errors.",
"author": { "name": "Deriv", "email": "api-support@deriv.com" },
"homepage": "https://developers.deriv.com",
Expand Down
43 changes: 30 additions & 13 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,11 @@ jobs:
permissions:
contents: write # commit the tree, cut the release
issues: write # file the failure issue
# GITHUB_TOKEN has no workflows: key; the App needs Workflows: write to push publish.yml.
steps:
# 1. Validate the dispatch payload BEFORE trusting anything in it.
- name: Validate dispatch payload
id: payload
env:
SOURCE_REPO: ${{ github.event.client_payload.source_repo }}
EXPECTED_SOURCE: ${{ vars.PROMOTION_SOURCE_REPO }}
Expand All @@ -67,18 +69,19 @@ jobs:
[ "${ARTIFACT:-}" = "${EXPECTED_ARTIFACT}" ] || fail "artifact_name '${ARTIFACT:-}' != expected '${EXPECTED_ARTIFACT}'"
[ -n "${NOTES:-}" ] || fail "release notes are empty"
echo "payload OK: ${SOURCE_REPO} ${TAG} ${ARTIFACT}"
echo "source_repo_name=${EXPECTED_SOURCE##*/}" >> "$GITHUB_OUTPUT"

# 2. Mint a short-lived App installation token (never a PAT).
- name: Mint GitHub App token
id: app-token
uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v2.0.2
uses: actions/create-github-app-token@3ff1caaa28b64c9cc276ce0a02e2ff584f3900c5 # v2.0.2
with:
app-id: ${{ secrets.CLIENT_ID_GHAPP_WRITE }}
private-key: ${{ secrets.PRIVATE_KEY_GHAPP_WRITE }}
# The App is installed on both the source (contents:read) and this repo
# (contents:write). Scope the token to the source for the download.
# (contents:write + Workflows: write). GITHUB_TOKEN does not push this file.
owner: ${{ github.repository_owner }}
repositories: ${{ github.event.repository.name }},${{ github.event.client_payload.source_repo_name }}
repositories: ${{ github.event.repository.name }},${{ steps.payload.outputs.source_repo_name }}

- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
Expand Down Expand Up @@ -108,7 +111,8 @@ jobs:
# Must be a gzip tarball.
gzip -t "$asset" || fail "artifact is not valid gzip"
# Reject absolute paths or `..` traversal in any member.
if tar -tzf "$asset" | grep -Eq '(^/|(^|/)\.\.(/|$))'; then
listing=$(tar -tzf "$asset")
if grep -Eq '(^/|(^|/)\.\.(/|$))' <<<"$listing"; then
fail "artifact contains an absolute path or a '..' traversal member"
fi

Expand All @@ -117,12 +121,18 @@ jobs:
- name: Unpack and re-verify the surface
run: |
set -euo pipefail
fail() { echo "FAIL: $1" >&2; exit 1; }
asset="_incoming/${EXPECTED_ARTIFACT}"
listing=$(tar -tvzf "$asset")
if grep -Eq '^[^-d]' <<<"$listing"; then
fail "artifact contains a non-regular, non-directory member (link/device/fifo)"
fi
rm -rf _staged && mkdir _staged
tar -xzf "_incoming/${EXPECTED_ARTIFACT}" -C _staged
# Re-run the surface guard against the unpacked tree. Use --dir (a
# filesystem walk): the unpacked tree is not git-tracked, so the
# default `git ls-files` mode would see zero files and pass vacuously.
node _staged/.github/workflows/verify-surface.mjs --dir _staged \
tar -xzf "$asset" -C _staged
# Re-run the checked-out surface guard against the unpacked tree. Use
# --dir (a filesystem walk): the unpacked tree is not git-tracked, so
# the default `git ls-files` mode would see zero files and pass vacuously.
node .github/workflows/verify-surface.mjs --dir _staged \
|| { echo "FAIL: unpacked tree failed the surface guard" >&2; exit 1; }

# 5. Commit the tree idempotently (only if it changed) — replace, don't
Expand All @@ -132,18 +142,25 @@ jobs:
TAG: ${{ github.event.client_payload.tag }}
run: |
set -euo pipefail
# Replace the tracked runtime surface with the staged tree, keeping the
# repo's own .github/ and .git/.
fail() { echo "FAIL: $1" >&2; exit 1; }
deletes=$(rsync -a --delete --dry-run --itemize-changes \
--exclude '.git' --exclude '.github' \
--exclude '_staged' --exclude '_incoming' \
_staged/ ./ | grep -F '*deleting' || true)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Nit: under pipefail, the trailing || true also swallows an rsync failure in the dry-run, not just grep's no-match exit. Harmless in practice since the real rsync below would then fail under set -e, but if you want the guard to be strict you could check PIPESTATUS[0] or run the dry-run to a file first.

if [ -n "$deletes" ]; then
fail "rsync --delete would remove extra public paths: ${deletes}"
fi
rsync -a --delete \
--exclude '.git' --exclude '.github' \
--exclude '_staged' --exclude '_incoming' \
_staged/ ./
cp _staged/.github/workflows/publish.yml .github/workflows/publish.yml

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Nit / question: only publish.yml is synced from _staged/.github/, but verify-surface.mjs, verify-surface.yml, and update-changelog.mjs (which this workflow depends on) are not — this PR carries a verify-surface.mjs change by hand that the steady-state receiver wouldn't pick up. If "the artifact must never replace its own verifier in the same run" is the intent, a short comment here would make that explicit. Either way, a [ -f _staged/.github/workflows/publish.yml ] || fail "artifact is missing publish.yml" guard would give a legible error instead of a bare cp failure under set -e.

git config user.name "deriv-api-plugin-bot"
git config user.email "api-support@deriv.com"
if git diff --quiet && git diff --cached --quiet; then
git add -A -- ':!_staged' ':!_incoming'
if git diff --cached --quiet; then
echo "no surface change for ${TAG}; nothing to commit"
else
git add -A -- ':!_staged' ':!_incoming'
git commit -m "chore: promote runtime surface ${TAG}"
git push
fi
Expand Down
41 changes: 34 additions & 7 deletions .github/workflows/verify-surface.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,29 @@
// leaves the source repo. This public checker carries NO internal-hostnames gate at
// all, so it discloses none of those internal names.
//
// Two fail-closed checks over the public tree:
// Fail-closed checks over the public tree:
// 1. Surface: every path is an allowlisted runtime path OR under this repo's
// own `.github/`. Anything else fails and is named.
// 2. Content gates: secrets/key material, tooling/planning artefacts, and
// 2. Presence: SECURITY.md, PRIVACY.md, and .mcp.json must exist
// (independent of ALLOW).
// 3. MCP URL pin: when .mcp.json is present, mcpServers.deriv must be
// type "http" at https://mcp-api-v2.deriv.com/mcp. No hostname-pattern gate.
// 4. Content gates: secrets/key material, tooling/planning artefacts, and
// local-runtime remnants — any hit fails and is named. (Internal hostname /
// repo-name gating is private-side only, per the note above, so this public
// checker discloses none of those names.) The `.github/` CI machinery is
// exempt from the content grep (it carries the gate patterns themselves and
// automation tokens); it is covered by the surface check instead.
// 5. Internal-reference scan over this repo's own `.github/` machinery.
// 6. Codex listing contract.
//
// File source (two modes):
// - default: the committed tree via `git ls-files` (public-repo CI).
// - `--dir <path>`: a FILESYSTEM WALK of <path> (used by publish.yml to check a
// freshly-unpacked, not-yet-tracked staging tree — `git ls-files` there would
// return empty and pass vacuously, a silent failure).
//
// Contract: exit 0 when both checks pass; exit non-zero and print `FAIL:` lines
// Contract: exit 0 when all checks pass; exit non-zero and print `FAIL:` lines
// otherwise.

import { existsSync, readFileSync, readdirSync } from 'node:fs';
Expand Down Expand Up @@ -226,7 +232,28 @@ for (const path of paths) {
fail(`unexpected path outside the allowlist (+ .github/): "${path}"`);
}

// 2. Content gates over the runtime surface (skip the .github/ machinery).
// 2. Presence gate.
for (const file of ['SECURITY.md', 'PRIVACY.md', '.mcp.json']) {
if (!existsSync(join(root, file))) fail(`required file missing: ${file}`);
}

const PRODUCTION_MCP_URL = 'https://mcp-api-v2.deriv.com/mcp';
// 3. MCP URL pin.
if (existsSync(join(root, '.mcp.json'))) {
try {
const mcp = JSON.parse(readFileSync(join(root, '.mcp.json'), 'utf8'));
const deriv = mcp && mcp.mcpServers && mcp.mcpServers.deriv;
if (!deriv || deriv.type !== 'http' || deriv.url !== PRODUCTION_MCP_URL) {
fail(
`MCP URL pin: mcpServers.deriv must have type "http" and url ${PRODUCTION_MCP_URL}`,
);
}
} catch (err) {
fail(`MCP URL pin: .mcp.json is not valid JSON (${err.message})`);
}
}

// 4. Content gates over the runtime surface (skip the .github/ machinery).
for (const path of paths) {
if (path === '.github' || path.startsWith('.github/')) continue;
const base = path.split('/').pop();
Expand All @@ -247,7 +274,7 @@ for (const path of paths) {
}
}

// 3. Internal-reference scan over this repo's own .github/ machinery. The content
// 5. Internal-reference scan over this repo's own .github/ machinery. The content
// gates above skip .github/ (it carries the gate patterns themselves), so this
// narrower scan covers the one thing that must never appear there: references
// to the source repository's private planning documents, issue numbers, or
Expand All @@ -274,7 +301,7 @@ for (const path of paths) {
}
}

// 4. Codex listing contract. Official package rules require a relative
// 6. Codex listing contract. Official package rules require a relative
// branding path (not an HTTPS URL), MCP declared as ./.mcp.json, and
// directory-submission length limits on the title and subtitle.
const CODEX_CATEGORIES = new Set([
Expand Down Expand Up @@ -407,4 +434,4 @@ if (failures.length > 0) {
console.error(`\nverify-surface: ${failures.length} problem(s) found`);
process.exit(1);
}
console.log(`verify-surface: OK — ${paths.length} path(s), surface + content + internal-reference gates clean`);
console.log(`verify-surface: OK — ${paths.length} path(s), surface + presence + MCP URL pin + content + internal-reference + Codex gates clean`);
15 changes: 10 additions & 5 deletions .github/workflows/verify-surface.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,16 @@
# self-contained — it inlines the same allowlist and content-gate patterns. Keep
# it in step with the source repository's promotion tooling when either changes.
#
# Two checks, both FAIL CLOSED:
# 1. Every tracked path is an allowlisted runtime path OR under this repo's own
# .github/. Anything else fails and is named.
# 2. The content gates (internal hostnames, secrets, tooling/planning
# artefacts, local-runtime remnants) re-run over the tree; any hit fails.
# Checks, all FAIL CLOSED:
# 1. Surface: every tracked path is an allowlisted runtime path OR under this
# repo's own .github/. Anything else fails and is named.
# 2. Presence: SECURITY.md, PRIVACY.md, and .mcp.json must exist.
# 3. MCP URL pin: mcpServers.deriv is type "http" at
# https://mcp-api-v2.deriv.com/mcp.
# 4. Content gates: secrets, tooling/planning artefacts, local-runtime
# remnants. No internal-hostnames gate on this public checker.
# 5. Internal-reference scan over .github/.
# 6. Codex listing contract.

name: verify-surface

Expand Down
20 changes: 13 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ install the skills separately.
```

The first command registers the public repository as a plugin marketplace; the
second installs the plugin from it. No Node or Python is required. Claude Code
connects to the plugin's server without a separate approval prompt.
second installs the plugin from it. No Node or Python is required.
Installing the plugin approves its MCP server once, at install time;
Claude Code may still prompt per tool call.

### Cursor

Expand All @@ -38,6 +39,9 @@ codex plugin marketplace add deriv-com/deriv-api-plugin
That registers this repository as a Codex marketplace. Install **deriv** from the
Plugins Directory, then restart Codex so it picks up the hosted MCP server. To
refresh an existing install after a listing change, run `codex plugin marketplace upgrade`.
If Codex shows an authentication step during install, that is Codex's
generic MCP install flow — the plugin itself needs no Deriv sign-in
and asks for no API token.

### Verify it works

Expand Down Expand Up @@ -95,11 +99,13 @@ If the plugin is already installed on that host, do not also copy skills or run

Once installed, the plugin's MCP tools, the Cursor rule, and the bundled skills
are available to the agent automatically. **No credentials and no configuration
are required** — the hosted server reads public Deriv API documentation and
schemas to answer questions and validate payloads; it does not sign in, store
keys, or ask you to set anything up. When you build an integration that itself
calls the Deriv API, your own application supplies its own credentials; the
plugin never handles them.
are required** — the plugin requires no Deriv account credentials. The hosted
server reads public Deriv API documentation and schemas to answer questions and
validate payloads; it does not sign in, store keys, or ask you to set anything
up. Tool-call arguments, including a `validate_payload` body, reach the hosted
server, so keep API tokens and other secrets out of tool-call arguments.
When you build an integration that itself calls the Deriv API, your own
application supplies its own credentials.

## Where to go next

Expand Down
22 changes: 14 additions & 8 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,25 @@ your MCP host ⟷ Deriv-hosted read-only MCP server ⟷ developers.deriv.com
- **Client to hosted server.** The MCP host connects to the Deriv-hosted MCP
server whose URL is declared in `.mcp.json` over **HTTPS**. The connection is
**unauthenticated and credential-free**: no authentication is required to call
the server, and none is accepted. The plugin does not sign in to Deriv, does
not read or write any API tokens, and asks you to configure nothing.
the server, and none is accepted. The plugin does not sign in to Deriv and
does not collect API tokens. It asks you to configure nothing. There are
no user Deriv credentials in this plugin.
The hosted server's own deployment secrets are never part of the plugin;
deployment secrets are not in this repo.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Nit: these two sentences say the same thing ("deployment secrets are never part of the plugin" / "deployment secrets are not in this repo"). One of them can go.

- **Hosted server to Deriv docs.** The server fetches from a **single approved
origin**, `developers.deriv.com`, and no other. It reads public documentation
and schemas only.

The hosted server is **read-only**. None of its tools writes, holds a
credential, signs in, or places a trade — the tools search endpoints, read
schemas, fields, and worked examples, validate payloads, and serve task-based
guidance. The server holds no secrets.
guidance.

The plugin does not collect, store, or send the user's Deriv account credentials.
Tool-call arguments, including a `validate_payload` body, reach the hosted
server, so keep API tokens and other secrets out of tool-call arguments.
If your own application authenticates to Deriv — for example via OAuth — that is
your application's concern. Those credentials belong to your application and are
never handled, stored, or transmitted by this plugin.
your application's concern.

## Failure modes

Expand All @@ -50,9 +55,10 @@ tell how current the underlying documentation is.
## Reporting a vulnerability

If you believe you have found a security issue, please report it privately to
the maintaining team, **`@deriv_api_v2_team`**, rather than opening a public
issue. Include enough detail to reproduce the problem and, where relevant, the
potential impact. The team will acknowledge the report and follow up on a fix.
**api-support@deriv.com** (Slack **`@deriv_api_v2_team`**), rather than
opening a public issue. Include enough detail to reproduce the problem and,
where relevant, the potential impact. The team will acknowledge the report
and follow up on a fix.

> Note: `mcp-api.deriv.com` is a separate Deriv MCP operated by a different team
> and is not the server this plugin points at.
2 changes: 1 addition & 1 deletion plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
"name": "deriv",
"description": "Live Deriv API schemas, payload validation, and guides for authentication, subscriptions, trades, and errors.",
"version": "1.0.1",
"version": "1.0.2",
"author": { "name": "Deriv", "email": "api-support@deriv.com" },
"homepage": "https://developers.deriv.com",
"repository": "https://github.com/deriv-com/deriv-api-plugin",
Expand Down
2 changes: 2 additions & 0 deletions skills/deriv-auth/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ If those tools are unavailable, use the live [authentication](https://developers

Apply the browser OAuth/BFF boundary below to greenfield browser applications and to work explicitly scoped as an authentication migration. An existing repository may preserve its established component ownership and session topology when re-platforming is outside scope, but this is an architecture-only exception. It never permits Legacy API endpoints, WebSocket authentication, request fields, or response assumptions. If the shared seam violates the New API contract, migrate it once before extending it; do not add a second credential path.

This architecture-only exception never includes storing Deriv access tokens, refresh tokens, or PATs in localStorage, in SPA-held session state, or in non-HttpOnly cookies. If an existing app does that, report the gap; do not extend the exception to cover it.

- Generate fresh cryptographically random PKCE verifier, challenge, and state per attempt. Store pending transactions briefly, server-side when the BFF owns initiation or in `sessionStorage` when the browser owns initiation, and consume them once.
- Validate state before trusting either a success or error callback. Exchange the code immediately and clean OAuth parameters from every terminal callback path while preserving unrelated URL state.
- Perform the code exchange on a backend. Keep Deriv access/refresh tokens in protected server-side storage. For a browser/BFF deployment, give the browser an opaque application session in a `Secure`, `HttpOnly` cookie with an appropriate `SameSite` policy and CSRF protection.
Expand Down
2 changes: 1 addition & 1 deletion skills/deriv-lightweight-charts/agents/openai.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
interface:
display_name: "Lightweight Charts Price Chart"
display_name: "Lightweight Charts price chart"
short_description: "Add TradingView's Lightweight Charts fed by the app's shared socket"
default_prompt: "Use $deriv-lightweight-charts to add a slim TradingView chart with barriers and trade markers to my Deriv trading app."
dependencies:
Expand Down
2 changes: 2 additions & 0 deletions skills/deriv-lightweight-charts/references/setup-and-feed.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Setup and feed: one series, one socket

> **Version anchor.** The library behaviour this file records — the v5 `addSeries` API in place of v4's `add…Series()` methods, the production build's `Cannot update oldest data` throw, the development-only sort assertion, and whitespace rows counting as points — was observed against `lightweight-charts@5.2.1`, the version this skill was authored and reviewed against between 2026-09-08 and 2026-09-09. The two `pip_size` meanings were read from the live API on 2026-09-09. Re-confirm every option name against the installed `typings.d.ts`: a major bump changes these APIs, as the v4-to-v5 rename recorded here shows.

The library exposes a chart, series on that chart, and two data calls per series: `setData` for the full history and `update` for the latest point. The adapter's job is to fill those from the application's existing tick-history request and subscription on the shared WebSocket, and nothing else.

## Install and create
Expand Down
Loading
Loading