From afd67d068aad11d8dfff25841922e720317a7fb7 Mon Sep 17 00:00:00 2001 From: Gordon Farquharson Date: Wed, 23 Sep 2026 13:25:41 +0100 Subject: [PATCH 1/4] pin SDK sources to latest-release fastedge-test, fastedge-sdk-js, fastedge-sdk-rust and proxy-wasm-sdk-as now sync from their latest GitHub release instead of main/master, so reference docs track what users can install and only regenerate on a new release. fastedge-templates stays on main (no releases; portal deploys from main). --- sources.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sources.json b/sources.json index 5f0accd..c5aa49f 100644 --- a/sources.json +++ b/sources.json @@ -4,7 +4,7 @@ { "id": "fastedge-test", "github_url": "https://github.com/G-Core/fastedge-test", - "ref": "main", + "ref": "latest-release", "trigger": "schedule", "contract_path": "fastedge-plugin-source/", "intent_dir": "agent-intent-skills/fastedge-test/", @@ -14,7 +14,7 @@ { "id": "fastedge-sdk-js", "github_url": "https://github.com/G-Core/FastEdge-sdk-js", - "ref": "main", + "ref": "latest-release", "trigger": "schedule", "contract_path": "fastedge-plugin-source/", "intent_dir": "agent-intent-skills/fastedge-sdk-js/", @@ -24,7 +24,7 @@ { "id": "fastedge-sdk-rust", "github_url": "https://github.com/G-Core/FastEdge-sdk-rust", - "ref": "main", + "ref": "latest-release", "trigger": "schedule", "contract_path": "fastedge-plugin-source/", "intent_dir": "agent-intent-skills/fastedge-sdk-rust/", @@ -34,7 +34,7 @@ { "id": "proxy-wasm-sdk-as", "github_url": "https://github.com/G-Core/proxy-wasm-sdk-as", - "ref": "master", + "ref": "latest-release", "trigger": "schedule", "contract_path": "fastedge-plugin-source/", "intent_dir": "agent-intent-skills/proxy-wasm-sdk-as/", From 5c05ec34e5901d9c9f9d570feb2a25429b46ac45 Mon Sep 17 00:00:00 2001 From: Gordon Farquharson Date: Wed, 23 Sep 2026 14:35:36 +0100 Subject: [PATCH 2/4] platform constraints docs to increase plugin knowledge base --- .../skills/fastedge-docs/SKILL.md | 4 +- .../reference/platform/cdn-filter-runtime.md | 122 ++++++++++++++++++ .../reference/platform/cdn-integration.md | 17 ++- .../reference/platform/error-codes.md | 16 ++- .../reference/platform/overview.md | 8 ++ .../reference/platform/storage.md | 105 +++++++++++++++ 6 files changed, 267 insertions(+), 5 deletions(-) create mode 100644 plugins/gcore-fastedge/skills/fastedge-docs/reference/platform/cdn-filter-runtime.md create mode 100644 plugins/gcore-fastedge/skills/fastedge-docs/reference/platform/storage.md diff --git a/plugins/gcore-fastedge/skills/fastedge-docs/SKILL.md b/plugins/gcore-fastedge/skills/fastedge-docs/SKILL.md index 9c79b0d..3ade6a6 100644 --- a/plugins/gcore-fastedge/skills/fastedge-docs/SKILL.md +++ b/plugins/gcore-fastedge/skills/fastedge-docs/SKILL.md @@ -68,7 +68,9 @@ The reference directory is organised in two layers: - `./reference/platform/overview.md` — Architecture, PoPs, app types, request lifecycle, resource limits - `./reference/platform/error-codes.md` — 530–533 debugging strategies -- `./reference/platform/cdn-integration.md` — How CDN apps attach to CDN resources via `options.fastedge`, lifecycle hook configuration, ruleset-based path overrides (replace-not-merge), public-route disable pattern +- `./reference/platform/cdn-filter-runtime.md` — **MANDATORY before designing, writing, or reviewing any CDN app (proxy-wasm filter).** Filter-vs-HTTP-app capability boundary, which request properties/headers are reliable, path non-normalisation (auth-bypass risk), filters-before-cache, what local tests cannot prove. +- `./reference/platform/storage.md` — **Read before designing anything that uses KV or Cache.** KV per-node read caching, write API shape, missing operations, Cache TTL/`incr` rules, failure rate, measured performance, preprod-vs-production. +- `./reference/platform/cdn-integration.md` — How CDN apps attach to CDN resources via `options.fastedge`, lifecycle hook configuration, ruleset-based path overrides (replace-not-merge), API merge semantics, purge after rule changes, public-route disable pattern - `./reference/platform/operations.md` — Operational knobs with time-bounded behaviour (the 30-min `debug` logging toggle) - `./reference/platform/best-practices.md` — Agent-quality guidance: confirmation discipline, scaffold-first, TDD loop, resource preconditions, observation vs. request, ask-don't-guess - `./reference/platform/as-constraints.md` — **MANDATORY before writing or reviewing any AssemblyScript CDN app code.** Hard compile-time and runtime constraints where AssemblyScript diverges from TypeScript. Violating these produces wasm that traps at runtime or silently returns wrong values — not a compiler error. diff --git a/plugins/gcore-fastedge/skills/fastedge-docs/reference/platform/cdn-filter-runtime.md b/plugins/gcore-fastedge/skills/fastedge-docs/reference/platform/cdn-filter-runtime.md new file mode 100644 index 0000000..3b7560a --- /dev/null +++ b/plugins/gcore-fastedge/skills/fastedge-docs/reference/platform/cdn-filter-runtime.md @@ -0,0 +1,122 @@ +# CDN Filter Runtime — Capabilities and Traps + +What a proxy-wasm filter (CDN app) can and cannot do on FastEdge, and the host behaviours that silently differ from generic proxy-wasm or from HTTP apps. **Read this before designing or reviewing a CDN app** — most items here are invisible in local tests and only surface on a deployed app. + +Every claim carries its provenance: + +| Tag | Meaning | +|---|---| +| `[live]` | Measured against a deployed app on a real CDN resource | +| `[source]` | Read from the FastEdge runtime or SDK source | + +Last verified: 2026-08-25 (SDK surface re-checked 2026-09-23). + +--- + +## Capability boundary — filter vs HTTP app + +A filter is a core wasm module linked against the proxy-wasm `env` ABI. It is a different execution world from an HTTP app, not a subset of it. Designs that assume symmetry with the HTTP-app SDK fail at deploy. + +| Capability | HTTP app | CDN filter | +|---|---|---| +| KV read | ✅ | ✅ same six operations: `open · get · scan · zrange_by_score · zscan · bf_exists` `[source]` | +| KV write | ❌ (API only — see [storage.md](./storage.md)) | ❌ (API only) | +| Cache (`get/set/delete/exists/incr/expire/purge/purgePrefix`) | ✅ | ✅ host-supported; **SDK support varies — check the SDK reference for your language** | +| Secrets / dictionary | ✅ | ✅ via the SDK's proxy-wasm module | +| Outbound `fetch` | ✅ | proxy-wasm `http_call` only | +| Deferred / background work | JS `waitUntil` only (not a timer) | ❌ none | +| State between requests | ❌ | ❌ — consecutive requests hit different nodes `[live]` | + +### Use only the SDK's proxy-wasm modules + +HTTP-app host APIs are component-model (WIT) imports. A proxy-wasm host cannot resolve them, so **one reference anywhere in a filter makes the app fail to instantiate — HTTP 530 on every path**, including paths that never reach the call `[live]`. It compiles and links with no warning. + +- **Rust:** in a filter import only from `fastedge::proxywasm::*`. Never use a top-level `fastedge::` module (`fastedge::cache`, `fastedge::secret`, …) — each has a `fastedge::proxywasm::` counterpart. +- **Check before deploying** — any `gcore:fastedge/...` import in a filter build means a 530 at runtime: + ```bash + wasm-tools print app.wasm | grep -o '(import "[^"]*"' | sort -u + ``` + +--- + +## Reading the request — use properties, not headers + +Several headers a proxy-wasm developer would reach for are mangled or absent. **All failures are silent** `[live]`. + +| Read via | Result | +|---|---| +| property `request.host` | ✅ clean client-requested host (before any origin host rewrite) | +| property `request.x_real_ip` | ✅ true client IP, IPv4 and IPv6 | +| property `request.path` | ⚠️ **includes the query string** — see below | +| property `request.query` | ✅ query without `?`; **absent** (not empty) when there is no query | +| property `request.asn` / `.country` / `.city` / `.region` / `.continent` | ✅ | +| property `source.address` | ❌ does not exist (Envoy's name) — returns nothing | +| header `host` | ❌ mangled to `_cache_sharded` | +| headers `:path`, `:authority` | ❌ absent | +| headers `x-real-ip`, `x-forwarded-for`, `cookie`, `user-agent` | ✅ intact (`cookie` is visible even with `ignore_cookie: true`) | + +### `request.path` includes the query string + +``` +GET /x?a=1&b=2 +request.path = "/x?a=1&b=2" +request.query = "a=1&b=2" +``` + +Prefix checks are unaffected; **equality checks silently fail on any request with a query** — auth callbacks are exactly where equality checks live. Split on `?` first. Corollary: because it is path+query, `request.path` works directly as a relative `Location` for a same-origin redirect, which avoids the mangled `host` header. + +--- + +## Security — gating filters + +### The CDN does not normalise the path before the filter runs + +`[live]` Traversal and separator sequences arrive verbatim: + +```bash +curl --path-as-is ".../app/x/../test" # request.path = "/app/x/../test" +curl --path-as-is ".../app//test" # request.path = "/app//test" +``` + +A filter that bypasses auth on a prefix (`/auth/`, `/public/`) will wave through `/auth/../admin`, which the origin may normalise back to `/admin`. **That is an authentication bypass**, and it is the natural way to write the code. + +Reject suspicious sequences before any prefix bypass, and **fail closed** — fall through to the auth check. Never normalise the path yourself and proceed; you will not match the origin's rules. + +```rust +fn has_suspicious_sequence(path: &str) -> bool { + if path.contains("..") || path.contains("//") || path.contains('\\') { return true; } + let lower = path.to_ascii_lowercase(); + lower.contains("%2e") || lower.contains("%2f") + || lower.contains("%5c") || lower.contains("%25") // %25 catches double-encoding +} +``` + +### Filters run before the CDN cache lookup + +`[live, edge tier only]` A request-phase filter runs on **every** request, including cache hits. + +- **Security:** a cached response cannot bypass a request-phase gate. +- **Cost:** every request pays for whatever the filter does — KV reads, cache ops — even on cache hits. +- A `no-store` response from a filter is not cached. Varying the cache key on a session cookie fragments the cache per visitor. + +Only `execute_on_edge` was measured; ordering on the shield tier is unverified. + +--- + +## Synthetic responses + +`[live]` A filter can serve a complete response via `send_http_response`: any status, arbitrary headers, multiple `Set-Cookie` headers on one response, and bodies up to at least 1 MB without truncation. Single-app designs (challenge pages, interstitials, error pages) do not need a second HTTP app. + +--- + +## What local testing cannot prove + +`@gcoredev/fastedge-test` runs proxy-wasm filters locally and proves **flow logic** — routing, header manipulation, gate decisions. It cannot prove **host behaviour**: property semantics, header mangling, path non-normalisation, instantiation failures, cache interaction. Every trap on this page passes a local suite green. Confirm host-dependent behaviour on a deployed app attached to a real CDN resource. + +--- + +## See Also + +- [storage.md](./storage.md) — KV and Cache semantics, limits, measured performance +- [cdn-integration.md](./cdn-integration.md) — attaching filters to CDN resources +- [error-codes.md](./error-codes.md) — 530 diagnosis diff --git a/plugins/gcore-fastedge/skills/fastedge-docs/reference/platform/cdn-integration.md b/plugins/gcore-fastedge/skills/fastedge-docs/reference/platform/cdn-integration.md index 2024764..89e14f6 100644 --- a/plugins/gcore-fastedge/skills/fastedge-docs/reference/platform/cdn-integration.md +++ b/plugins/gcore-fastedge/skills/fastedge-docs/reference/platform/cdn-integration.md @@ -181,8 +181,23 @@ curl -s -X PATCH "https://api.gcore.com/cdn/resources/" \ }' ``` +**`PATCH /cdn/resources/{id}` merges `options`** — keys you omit (e.g. `hostHeader`) are kept `[live]`. This is the opposite of `PATCH /fastedge/v1/apps/{id}`, which **replaces `env` wholesale**. Do not confuse either with the rule-vs-resource replace semantics above, which govern request-time override, not the API. + Rules are managed through their own endpoints under `/cdn/resources/{resource_id}/rules`. The full rule API is in the Gcore CDN documentation. +### Purge after changing rules or attachments + +A rule or attachment change is not visible for content already in the CDN cache — the old response keeps being served until it expires (observed ~15 min). After a change, purge and then verify: + +```bash +curl -s -X POST "https://api.gcore.com/cdn/resources//purge" \ + -H "Authorization: APIKey $GCORE_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{"paths": []}' # empty list purges everything +``` + +The FastEdge MCP server's default access policy blocks this endpoint; tell the user to run the purge themselves when it is blocked. + ## Operational Notes - **`app_id` is a string.** The FastEdge apps API (`GET /fastedge/v1/apps`) returns `id` as an integer, but in the `options.fastedge..app_id` field it must be quoted as a string. Mixing types is a common source of validation errors. @@ -191,7 +206,7 @@ Rules are managed through their own endpoints under `/cdn/resources/{resource_id - **Hook absence vs. `enabled: false`.** Both result in the hook not running. Removing the key entirely is cleaner; toggling `enabled: false` preserves the hook's other config for easy re-enable. - **`execute_on_shield` only matters when origin shielding is on.** If `shielded: false` on the resource, the shield-layer setting has no effect — the hook only ever runs at the edge. - **Rule weight ordering.** When multiple rules match a request path, only the rule with the lowest weight applies. Rules don't compose. -- **Changes propagate.** CDN configuration changes can take a few minutes to propagate to all PoPs. The resource may briefly be in `status: "processed"` after an update. +- **Changes propagate.** CDN configuration changes can take a few minutes to propagate to all PoPs. The resource may briefly be in `status: "processed"` after an update. Cached content additionally needs a purge (above). ## See Also diff --git a/plugins/gcore-fastedge/skills/fastedge-docs/reference/platform/error-codes.md b/plugins/gcore-fastedge/skills/fastedge-docs/reference/platform/error-codes.md index 23b24f5..fb81210 100644 --- a/plugins/gcore-fastedge/skills/fastedge-docs/reference/platform/error-codes.md +++ b/plugins/gcore-fastedge/skills/fastedge-docs/reference/platform/error-codes.md @@ -10,7 +10,10 @@ FastEdge returns specific HTTP status codes (530-533) when the Wasm runtime enco **Meaning:** The Wasm module failed to start up before processing the request. +**Signal:** a CDN app that fails to instantiate also returns `x-cdn-internal-status: 3100`. That header distinguishes "failed to start" from "the app ran and errored" (531). + **Common Causes:** +- **Unresolved imports (CDN apps)** — the binary imports a host function the proxy-wasm host does not provide, most commonly an HTTP-app (component-model) API used in a filter, e.g. top-level `fastedge::cache` in Rust instead of `fastedge::proxywasm::cache`. **Every path returns 530**, including paths that never reach the call, and the build gives no warning. See [cdn-filter-runtime.md](./cdn-filter-runtime.md). - Missing required environment variables that the app reads during initialization - Corrupted or invalid Wasm binary - Binary was compiled for the wrong target (not `wasm32-wasip1`) @@ -21,9 +24,15 @@ FastEdge returns specific HTTP status codes (530-533) when the Wasm runtime enco - JS: `fastedge-build ./src/index.js ./.wasm` completed without errors - Rust: `cargo build --release --target wasm32-wasip1` succeeded 2. Check binary size: `ls -la *.wasm` or `ls -la target/wasm32-wasip1/release/*.wasm` -3. Test locally: `fastedge-run http -w ./app.wasm --port 8080` -4. Verify all required env vars are set via API or portal -5. Re-upload the binary and update the app +3. **CDN apps:** inspect the import table — any `gcore:fastedge/...` import in a proxy-wasm build is unresolvable: + ```bash + wasm-tools print app.wasm | grep -o '(import "[^"]*"' | sort -u + ``` +4. Run locally: + - HTTP apps: `fastedge-run http -w ./app.wasm --port 8080` + - CDN apps: `fastedge-run` does not run proxy-wasm — use `@gcoredev/fastedge-test`. Note a local run does not reproduce host instantiation, so a clean local run does not rule out step 3. +5. Verify all required env vars are set via API or portal +6. Re-upload the binary and update the app --- @@ -32,6 +41,7 @@ FastEdge returns specific HTTP status codes (530-533) when the Wasm runtime enco **Meaning:** The app started successfully but threw an unhandled exception during request processing. **Common Causes:** +- Calling `getEnv()` / `getSecret()` at module top level (JS) — they are request-time only; call them inside the handler - Uncaught JavaScript exception (TypeError, ReferenceError, etc.) - Rust panic (`unwrap()` on `None` or `Err`) - Failed `fetch()` call without error handling diff --git a/plugins/gcore-fastedge/skills/fastedge-docs/reference/platform/overview.md b/plugins/gcore-fastedge/skills/fastedge-docs/reference/platform/overview.md index 415dc14..7575396 100644 --- a/plugins/gcore-fastedge/skills/fastedge-docs/reference/platform/overview.md +++ b/plugins/gcore-fastedge/skills/fastedge-docs/reference/platform/overview.md @@ -99,6 +99,14 @@ What is appropriate: per-end-user or per-session key prefixes within your app's 5. **Response Delivery** — Response sent back to client 6. **Cleanup** — Wasm instance destroyed, memory freed +### No scheduled or deferred work + +There is no scheduler and no timer API. JS `event.waitUntil()` extends the instance until a promise settles but runs immediately after the response — `waitUntil(sleep(30_000).then(work))` just pins one instance per request for 30 s. Rust has no deferred-work API at all. Rewrite "do X in L seconds" as "on each request, do X if L has elapsed"; periodic jobs need an external cron. + +### PoP identity + +`getEnv("dc")` returns the PoP's lowercase short code (e.g. `ls1`, `am3`) — the only PoP identifier available to an app. **It can be empty**; an empty-string fallback silently merges every PoP's keys into one namespace. Fail loudly instead. Node count per PoP varies, and the number of active PoPs is not discoverable from inside an app. + ## Resource Limits | Resource | Basic Plan | Pro Plan | diff --git a/plugins/gcore-fastedge/skills/fastedge-docs/reference/platform/storage.md b/plugins/gcore-fastedge/skills/fastedge-docs/reference/platform/storage.md new file mode 100644 index 0000000..9127474 --- /dev/null +++ b/plugins/gcore-fastedge/skills/fastedge-docs/reference/platform/storage.md @@ -0,0 +1,105 @@ +# Storage Semantics — KV Store and Cache + +Behaviour of FastEdge's two storage primitives that the SDK references do not state: consistency, write shapes, TTL rules, missing operations, and measured performance. Applies to HTTP apps and CDN filters unless noted. For the per-language API, see the SDK reference for your language. + +| | KV Store | Cache | +|---|---|---| +| Scope | Global, account-level, assigned to apps | Per-PoP, shared by every node in the PoP `[source + told]` | +| Consistency | Eventually consistent; **reads cached per node** (below) | Strong within a PoP | +| Writes from app code | ❌ API only | ✅ | +| Atomic primitive | — | `incr` only | + +Provenance tags: `[live]` measured on a deployed app · `[source]` read from the runtime source · `[told]` stated by the FastEdge team · `[unverified]` not yet confirmed. Last verified: 2026-08-27. + +--- + +## KV Store + +### `get()` is a cached read — ~30 s per node + +`[live, production]` Each `get(key)` populates a per-node cache entry that lives ~30 s, **whether or not the key existed**. Until it expires, that node keeps returning the old result — the old value, or "absent" for a key that now exists. Sorted-set reads (`zrangeByScore`) are **not** cached and reflect writes in 0–1 s. + +| Reads before the write | Write | Visible after | +|---|---|---| +| none | create `kv` key | 1 s | +| heavy `get()` polling of the absent key | create `kv` key | **31 s** | +| heavy `get()` polling of the existing key | update its value | **30 s, then flapped** | +| heavy `zrangeByScore` polling | add `sorted_set` member | 0 s | + +- **Values flap.** Warm and expired nodes answer differently at the same instant, so a polled `kv` flag can turn on, off, and on again during the window. For access gating this is worse than a delay. +- **Polling makes it worse** — every poll refreshes the staleness hiding the change. +- **Absence is not a safe signal.** "Key missing ⇒ not started" fails in the direction of "my change hasn't happened". + +**Rule:** use `kv` + `get()` for values written once and read as configuration. For anything polled that must see changes promptly (a "has this happened yet?" flag), use a **sorted set with one member whose score is the timestamp** — uncached and consistent across nodes. + +### Writes go through the API only + +`[live]` `PUT /fastedge/v1/kv/{store_id}/data` — never from inside an app. Many entries can be batched in one call. + +```jsonc +[{ "op": "add", // add | del_key | del_entries — REQUIRED + "key": "…", + "datatype": "kv", // kv | sorted_set | bloom_filter + "payload": { "value": "…", "encoding": "plain" } }] // encoding: plain | base64 | sha256 +``` + +- **`sorted_set` nests differently:** its `payload` *is* the member array. The 400 only says `doesn't match any schema from "anyOf"`. To learn a payload shape, write one entry by hand and read it back with `GET /fastedge/v1/kv/{store}/data/{key}` — the read shape mirrors the write shape. +- **`datatype` scopes `del_key`.** Deleting a `kv` key with `datatype: "sorted_set"` returns **200 with `del_count: 0`** and removes nothing; a later `zrangeByScore` on that name then throws a type error. **Check `del_count`** on cleanup writes, especially when migrating a key between datatypes. +- The response includes `revision`, a store-wide monotonic counter — a freshness signal independent of clocks. +- Attaching a store to an app: `stores: {"": {"id": }}`; omit the inner `name` (read-only). + +### Missing operations + +`[source]` The read surface is exactly `open · get · scan · zrange_by_score · zscan · bf_exists`. There is **no** rank-based `ZRANGE`, `ZCOUNT`, `ZCARD` or `ZSCORE`. Do not emulate them in app code: `zrange_by_score(-inf, +inf)` transfers every member with its score into the wasm instance on every call. + +`zrange_by_score` on a missing key returns an empty list, not an error. + +--- + +## Cache + +### Interface and atomicity + +`[source]` `get · set · delete · exists · incr · expire · purge · purge_prefix`. **`incr` is the only atomic primitive** — no compare-and-set, `SETNX`, scripting, multi-key operations, or `SCAN`. Coordination must be expressible as "increment and compare the returned value". + +### TTL rules + +`[source + live]` + +- **TTLs are silently clamped** to a deployment ceiling (default 4 days). `set` without a TTL receives that default rather than "no expiry" — do not rely on an entry outliving it. There is no TTL-read primitive, so the clamp is invisible. +- **`incr` sets no TTL and does not index the key.** `incr`-created keys escape both the default-TTL cleanup and `purge_prefix` — measured still present after 22 days. **Always follow `incr` with `expire`**, whether the caller won or lost, or the key leaks permanently. +- `expire` returns `false` for an absent key and `Err` for a failure. Do not collapse the two. + +### Failures are real — retry + +`[live, production]` Cache operations fail outright with explicit errors (not timeouts) at ~0.44% overall, bursty and PoP-specific (one PoP showed 1.5%). A retry cap of 1 is not sound — use 3–4, and decide deliberately whether a failure fails open or closed. + +> **CDN filters:** the figures and TTL behaviour on this page were measured through HTTP apps. Proxy-wasm cache support is newer and is `[unverified]` to share the same backend behaviour. + +--- + +## Measured performance + +`[live, production]` Point-in-time, one account, mostly one PoP — evidence, not guarantees. + +| Metric | Value | +|---|---| +| `Cache.incr` | p50 1 ms · p95 27 ms · max 102 ms | +| `Cache.expire` | p50 2 ms · p95 6 ms · max 21 ms | +| KV write → visible (no prior `get()` polling) | 61 / 80 / 107 ms (min / p50 / max) | +| KV write rate, one client | sustained ~94 writes/s | +| KV sorted-set ingest | ~2,450 members/s; knee at 32–64 concurrent writers | +| Largest single batched `PUT` | ~1,560 entries — bounded by a 30 s gateway timeout, not payload size | +| Sorted-set storage | ~31 bytes/member | +| Intra-PoP clock skew | ≤5 ms, probably ≤1 ms | + +### Preprod is not production + +Preprod KV propagation was bimodal — typically ~210 ms, but ~1 in 8–15 writes invisible for over 45 s. Production did not do this. Do not benchmark or size designs on preprod. Before attributing a ~30 s delay to either, check the cached-read behaviour above: if something was polling the key with `get()`, or the value flaps once it appears, it is the per-node read cache. + +--- + +## See Also + +- [cdn-filter-runtime.md](./cdn-filter-runtime.md) — what a CDN filter can access +- [overview.md](./overview.md) — account-level KV stores, resource limits From 992d9c9a5259afc2040f93b52420dfa68c5eb223 Mon Sep 17 00:00:00 2001 From: Gordon Farquharson Date: Wed, 23 Sep 2026 15:41:49 +0100 Subject: [PATCH 3/4] copilot --- context/CONTEXT_INDEX.md | 7 ++----- .../skills/fastedge-docs/SKILL.md | 3 +++ .../reference/platform/cdn-filter-runtime.md | 4 ++-- .../reference/platform/cdn-integration.md | 14 ++++++++------ .../fastedge-docs/reference/platform/overview.md | 2 +- .../fastedge-docs/reference/platform/storage.md | 4 ++-- 6 files changed, 18 insertions(+), 16 deletions(-) diff --git a/context/CONTEXT_INDEX.md b/context/CONTEXT_INDEX.md index 8f28836..86e65be 100644 --- a/context/CONTEXT_INDEX.md +++ b/context/CONTEXT_INDEX.md @@ -97,8 +97,7 @@ Start here. Read only what your task requires. ### Post-Onboarding Cleanup -- **`sources.json`: fastedge-sdk-js and fastedge-sdk-rust refs are temporarily `"main"`** — changed from `"latest-release"` during onboarding because the `fastedge-plugin-source/` contracts didn't exist at the latest tagged releases. Once contracts are merged and new releases are cut, reset both to `"latest-release"` so doc syncs are release-gated. -- **`sources.json`: proxy-wasm-sdk-as ref is `"latest-release"`** — added April 2026. Pipeline test (AS-07) pending. Deploy workflow dispatch (AS-09) not yet wired. +- **`sources.json`: all SDK repos + fastedge-test are on `"latest-release"`** (Sep 2026) — every latest release now contains `fastedge-plugin-source/`, so doc syncs are release-gated. fastedge-templates stays on `"main"` (no releases; the portal deploys from main). ### MCP Integration — Planned (after 002-scaffold-redesign) @@ -114,6 +113,4 @@ Build/deploy delegation to FastEdge-mcp-server. Phase 1 (delegate build + deploy - Intent skills with hierarchical inheritance: root bases → appType bases → per-example files. 4th intent directory added for proxy-wasm-sdk-as (20 files). - Pipeline PRs merged for all 3 source repos (fastedge-test #35, fastedge-sdk-js #34, fastedge-sdk-rust #33/#36). proxy-wasm-sdk-as pending first run (AS-07). - Blueprint format contract: `specs/002-scaffold-redesign/contracts/blueprint-format.md` -- Dual-intent manifest pattern: `specs/002-scaffold-redesign/contracts/manifest-dual-intent.md` - -**Note:** `sources.json` refs for fastedge-sdk-js and fastedge-sdk-rust are still `"main"` — reset to `"latest-release"` once new releases are cut. +- Dual-intent manifest pattern: `specs/002-scaffold-redesign/contracts/manifest-dual-intent.md` \ No newline at end of file diff --git a/plugins/gcore-fastedge-codex/skills/fastedge-docs/SKILL.md b/plugins/gcore-fastedge-codex/skills/fastedge-docs/SKILL.md index 7823ff7..cb49354 100644 --- a/plugins/gcore-fastedge-codex/skills/fastedge-docs/SKILL.md +++ b/plugins/gcore-fastedge-codex/skills/fastedge-docs/SKILL.md @@ -24,6 +24,9 @@ Answer FastEdge questions with high precision and low token usage using local in - Before proposing to hand-build a capability from scratch, check for a matching `templates` topic in the index first — it may already exist as a maintained, ready-to-deploy bolt-on installed from the Gcore portal, not something to scaffold. +- Before designing, writing, or reviewing a CDN app (proxy-wasm filter), read the + `platform-cdn-filter-runtime` topic. Before designing anything that uses KV or Cache, read + `platform-storage`. Both record host behaviour that local tests cannot reveal. ## Output rules diff --git a/plugins/gcore-fastedge/skills/fastedge-docs/reference/platform/cdn-filter-runtime.md b/plugins/gcore-fastedge/skills/fastedge-docs/reference/platform/cdn-filter-runtime.md index 3b7560a..91b5ad4 100644 --- a/plugins/gcore-fastedge/skills/fastedge-docs/reference/platform/cdn-filter-runtime.md +++ b/plugins/gcore-fastedge/skills/fastedge-docs/reference/platform/cdn-filter-runtime.md @@ -45,10 +45,10 @@ Several headers a proxy-wasm developer would reach for are mangled or absent. ** | Read via | Result | |---|---| -| property `request.host` | ✅ clean client-requested host (before any origin host rewrite) | +| property `request.host` | ✅ clean client-requested host (before any origin host rewrite) on the edge tier; on shield nodes it may carry a `shield_` prefix | | property `request.x_real_ip` | ✅ true client IP, IPv4 and IPv6 | | property `request.path` | ⚠️ **includes the query string** — see below | -| property `request.query` | ✅ query without `?`; **absent** (not empty) when there is no query | +| property `request.query` | ✅ query without `?`; not set by the host when there is no query (how "not set" surfaces — `None`, empty buffer — depends on the SDK) | | property `request.asn` / `.country` / `.city` / `.region` / `.continent` | ✅ | | property `source.address` | ❌ does not exist (Envoy's name) — returns nothing | | header `host` | ❌ mangled to `_cache_sharded` | diff --git a/plugins/gcore-fastedge/skills/fastedge-docs/reference/platform/cdn-integration.md b/plugins/gcore-fastedge/skills/fastedge-docs/reference/platform/cdn-integration.md index 89e14f6..49aa783 100644 --- a/plugins/gcore-fastedge/skills/fastedge-docs/reference/platform/cdn-integration.md +++ b/plugins/gcore-fastedge/skills/fastedge-docs/reference/platform/cdn-integration.md @@ -181,22 +181,24 @@ curl -s -X PATCH "https://api.gcore.com/cdn/resources/" \ }' ``` -**`PATCH /cdn/resources/{id}` merges `options`** — keys you omit (e.g. `hostHeader`) are kept `[live]`. This is the opposite of `PATCH /fastedge/v1/apps/{id}`, which **replaces `env` wholesale**. Do not confuse either with the rule-vs-resource replace semantics above, which govern request-time override, not the API. +**`PATCH /cdn/resources/{id}` merges `options`** — keys you omit (e.g. `hostHeader`) are kept `[live]`. Do not confuse this with the rule-vs-resource replace semantics above, which govern request-time override, not the API. Rules are managed through their own endpoints under `/cdn/resources/{resource_id}/rules`. The full rule API is in the Gcore CDN documentation. -### Purge after changing rules or attachments +### Cached content after a routing change -A rule or attachment change is not visible for content already in the CDN cache — the old response keeps being served until it expires (observed ~15 min). After a change, purge and then verify: +Request-phase filters run before the cache lookup (see [cdn-filter-runtime.md](./cdn-filter-runtime.md)), so attaching or changing an `on_request_headers` filter takes effect on cached paths without a purge. What does **not** change on its own is content already cached from the previous configuration — e.g. after a rule that points a path at a different origin, or a change to response-phase output. That content keeps being served until it expires (observed ~15 min). + +In that case, purge only the affected paths, then verify: ```bash curl -s -X POST "https://api.gcore.com/cdn/resources//purge" \ -H "Authorization: APIKey $GCORE_API_KEY" \ -H "Content-Type: application/json" \ - -d '{"paths": []}' # empty list purges everything + -d '{"paths": ["/affected/path/*"]}' ``` -The FastEdge MCP server's default access policy blocks this endpoint; tell the user to run the purge themselves when it is blocked. +`{"paths": []}` purges **the entire resource** and sends all traffic back to origin — use it only with explicit user confirmation. The FastEdge MCP server's default access policy blocks this endpoint; when it is blocked, give the user the command to run themselves. ## Operational Notes @@ -206,7 +208,7 @@ The FastEdge MCP server's default access policy blocks this endpoint; tell the u - **Hook absence vs. `enabled: false`.** Both result in the hook not running. Removing the key entirely is cleaner; toggling `enabled: false` preserves the hook's other config for easy re-enable. - **`execute_on_shield` only matters when origin shielding is on.** If `shielded: false` on the resource, the shield-layer setting has no effect — the hook only ever runs at the edge. - **Rule weight ordering.** When multiple rules match a request path, only the rule with the lowest weight applies. Rules don't compose. -- **Changes propagate.** CDN configuration changes can take a few minutes to propagate to all PoPs. The resource may briefly be in `status: "processed"` after an update. Cached content additionally needs a purge (above). +- **Changes propagate.** CDN configuration changes can take a few minutes to propagate to all PoPs. The resource may briefly be in `status: "processed"` after an update. Content cached under the old configuration may also need a path-scoped purge (above). ## See Also diff --git a/plugins/gcore-fastedge/skills/fastedge-docs/reference/platform/overview.md b/plugins/gcore-fastedge/skills/fastedge-docs/reference/platform/overview.md index 7575396..78300ee 100644 --- a/plugins/gcore-fastedge/skills/fastedge-docs/reference/platform/overview.md +++ b/plugins/gcore-fastedge/skills/fastedge-docs/reference/platform/overview.md @@ -101,7 +101,7 @@ What is appropriate: per-end-user or per-session key prefixes within your app's ### No scheduled or deferred work -There is no scheduler and no timer API. JS `event.waitUntil()` extends the instance until a promise settles but runs immediately after the response — `waitUntil(sleep(30_000).then(work))` just pins one instance per request for 30 s. Rust has no deferred-work API at all. Rewrite "do X in L seconds" as "on each request, do X if L has elapsed"; periodic jobs need an external cron. +There is no scheduler or durable background execution. Timers exist within a request (e.g. JS `setTimeout`) but live only as long as that instance. JS `event.waitUntil()` extends the instance until a promise settles, starting immediately after the response — `waitUntil(sleep(30_000).then(work))` just pins one instance per request for 30 s. Rust has no post-response work API. Rewrite "do X in L seconds" as "on each request, do X if L has elapsed"; periodic jobs need an external cron. ### PoP identity diff --git a/plugins/gcore-fastedge/skills/fastedge-docs/reference/platform/storage.md b/plugins/gcore-fastedge/skills/fastedge-docs/reference/platform/storage.md index 9127474..dcd92b6 100644 --- a/plugins/gcore-fastedge/skills/fastedge-docs/reference/platform/storage.md +++ b/plugins/gcore-fastedge/skills/fastedge-docs/reference/platform/storage.md @@ -66,13 +66,13 @@ Provenance tags: `[live]` measured on a deployed app · `[source]` read from the `[source + live]` -- **TTLs are silently clamped** to a deployment ceiling (default 4 days). `set` without a TTL receives that default rather than "no expiry" — do not rely on an entry outliving it. There is no TTL-read primitive, so the clamp is invisible. +- **TTLs are silently clamped** to a deployment ceiling (default 4 days). The JS and Rust SDK references describe `set` without a TTL as "no expiry"; the host actually applies the default ceiling (runtime source, and a no-TTL `set` key measured gone within 22 days). **Treat the host behaviour as authoritative** — do not rely on any entry outliving ~4 days. There is no TTL-read primitive, so the clamp is invisible. - **`incr` sets no TTL and does not index the key.** `incr`-created keys escape both the default-TTL cleanup and `purge_prefix` — measured still present after 22 days. **Always follow `incr` with `expire`**, whether the caller won or lost, or the key leaks permanently. - `expire` returns `false` for an absent key and `Err` for a failure. Do not collapse the two. ### Failures are real — retry -`[live, production]` Cache operations fail outright with explicit errors (not timeouts) at ~0.44% overall, bursty and PoP-specific (one PoP showed 1.5%). A retry cap of 1 is not sound — use 3–4, and decide deliberately whether a failure fails open or closed. +`[live, production]` Cache operations fail outright with explicit errors (not timeouts) at ~0.44% overall, bursty and PoP-specific (one PoP showed 1.5%). Plan for retries rather than assuming success, but bound them by the remaining execution budget — a single op can take ~100 ms at the tail, beyond the Basic plan's 50 ms limit. Decide deliberately whether an exhausted retry fails open or closed. > **CDN filters:** the figures and TTL behaviour on this page were measured through HTTP apps. Proxy-wasm cache support is newer and is `[unverified]` to share the same backend behaviour. From bebf0d917d99eee216b91ba3f7a87a27314484a4 Mon Sep 17 00:00:00 2001 From: Gordon Farquharson Date: Thu, 24 Sep 2026 08:49:44 +0100 Subject: [PATCH 4/4] copilot - ttl --- .../skills/fastedge-docs/reference/platform/storage.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/plugins/gcore-fastedge/skills/fastedge-docs/reference/platform/storage.md b/plugins/gcore-fastedge/skills/fastedge-docs/reference/platform/storage.md index dcd92b6..e55ac0c 100644 --- a/plugins/gcore-fastedge/skills/fastedge-docs/reference/platform/storage.md +++ b/plugins/gcore-fastedge/skills/fastedge-docs/reference/platform/storage.md @@ -67,7 +67,9 @@ Provenance tags: `[live]` measured on a deployed app · `[source]` read from the `[source + live]` - **TTLs are silently clamped** to a deployment ceiling (default 4 days). The JS and Rust SDK references describe `set` without a TTL as "no expiry"; the host actually applies the default ceiling (runtime source, and a no-TTL `set` key measured gone within 22 days). **Treat the host behaviour as authoritative** — do not rely on any entry outliving ~4 days. There is no TTL-read primitive, so the clamp is invisible. -- **`incr` sets no TTL and does not index the key.** `incr`-created keys escape both the default-TTL cleanup and `purge_prefix` — measured still present after 22 days. **Always follow `incr` with `expire`**, whether the caller won or lost, or the key leaks permanently. +- **`incr` sets no TTL and does not index the key.** `incr`-created keys escape both the default-TTL cleanup and `purge_prefix` — measured still present after 22 days. Every `incr` key needs an `expire`, but *when* depends on the key shape — `expire` sets a deadline relative to now, so calling it again pushes the deadline out: + - **Fixed key** (e.g. `rl:`): call `expire` only when `incr` returns `1`, and retry that `expire` within the request budget. Expiring on every call resets the window and keeps the key alive under continuous traffic. If the one `expire` fails for good, the key never expires — there is no TTL-read to detect it. + - **Time-bucketed key** (window id in the name, e.g. `rl::`): calling `expire` on every call is safe and self-healing — writes stop when the window passes, so the key expires one TTL after its last write even if earlier `expire` calls failed. Prefer this shape when a permanently leaked key is costly. - `expire` returns `false` for an absent key and `Err` for a failure. Do not collapse the two. ### Failures are real — retry