mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-26 02:00:52 +01:00
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - A Cloud-managed instance injects an operator-owned HTML snippet before `</body>` through `injectCloudUiSnippet`. > - The Cloud control plane delivers that snippet to each instance as an environment variable through a provider API. > - The provider edge firewall now base64-decodes the request payload and blocks any value whose decoded form contains a `<script` marker. > - A working snippet needs a script tag, so every delivery is now blocked and the operator cannot ship the snippet at all. > - This pull request treats a resolved value that does not start with `<` as a bare script body and wraps it in a `<script>` element at injection time. > - The benefit is that the operator can deliver a tag-free body that the firewall passes, and the instance restores the script element on the page. ## Linked Issues or Issue Description No public issue exists. The problem is described below. Related PRs (searched the PR list; none duplicate this change): - Refs #13168 — added `injectCloudUiSnippet`, the mechanism this extends. - Refs #13245 — added the base64 `_B64` path on the assumption that base64 clears provider WAFs. That assumption no longer holds; this PR is the successor. - Refs #13441 — the in-product feedback approach that the Cloud-owned snippet replaced (closed). **What happened?** `injectCloudUiSnippet` injects `PAPERCLIP_CLOUD_UI_SNIPPET` (or the base64 `_B64` form) verbatim before `</body>`. A working value must therefore contain a `<script>` tag. The Cloud control plane delivers this value as an environment variable through a provider API that sits behind an edge firewall. The firewall now base64-decodes the payload and rejects any value whose decoded form contains `<script`. The delivery request fails, so the snippet cannot reach the instance. **Expected behavior** The operator can deliver the snippet through the provider API, and the instance runs it. **Steps to reproduce** 1. Build a snippet that contains a `<script>` element. 2. Deliver it to a Cloud instance through the provider variable API, in plain or base64 form. 3. The firewall rejects the request. The instance never receives the snippet. ## What Changed - `injectCloudUiSnippet` now wraps a resolved value that does not start with `<` in a `<script>...</script>` element. A value that already looks like markup is injected byte-for-byte, so existing full-`<script>` snippets are unchanged. - Added two unit tests: one for the wrap path (plain and base64), one that confirms `$&`-style replacement tokens in the body survive the wrap. ## Verification - `pnpm exec vitest run src/__tests__/cloud-ui-snippet.test.ts` — 17 pass. - `pnpm exec vitest run src/__tests__/static-index-html.test.ts` — 2 pass. - Confirmed the changed file has no type errors. The full `pnpm run typecheck` needs the Rust runner toolchain (`cargo`), which is absent on this machine; CI runs it in full. - Verified out of band that a tag-free body clears the provider firewall and wraps into valid, executable standalone JavaScript with no premature `</script>` close. ## Risks Low risk. The change adds a branch that only affects values that do not start with `<` — previously injected as inert text, never as a running script. Values that start with `<` keep their exact bytes. The content is trusted operator HTML, consistent with the existing contract. Roll back by reverting this commit. ## Model Used Claude — `claude-fable-5` (Fable 5), extended thinking, with tool use and code execution in Claude Code. A human author reviewed and verified the change before submission. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [ ] All Paperclip CI gates are green - [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [ ] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip <noreply@paperclip.ing>
84 lines
3.5 KiB
Markdown
84 lines
3.5 KiB
Markdown
# Cloud UI snippet
|
|
|
|
Cloud operators can set `PAPERCLIP_CLOUD_UI_SNIPPET` to an HTML snippet.
|
|
The server inserts it before `</body>` in static and Vite-served UI pages.
|
|
It requires the existing Cloud-managed instance signal. Self-hosted instances
|
|
ignore this setting. No snippet is enabled by default.
|
|
|
|
This is trusted deployment configuration, not user input. It executes in the
|
|
application origin and is visible to every browser that receives the UI shell.
|
|
Do not include secrets or customer data. Restart the app after changing it.
|
|
Operators must review scripts and any required CSP changes before deployment.
|
|
|
|
## Base64 variant
|
|
|
|
Delivery pipelines that write env vars through provider APIs can sit behind
|
|
web application firewalls that reject values containing raw script markup.
|
|
`PAPERCLIP_CLOUD_UI_SNIPPET_B64` carries the same snippet through them as
|
|
standard base64 of the UTF-8 HTML:
|
|
|
|
```sh
|
|
PAPERCLIP_CLOUD_UI_SNIPPET_B64="$(base64 < snippet.html)"
|
|
```
|
|
|
|
Whitespace and line wrapping in the value are tolerated. A value that is not
|
|
canonical padded base64 of UTF-8 text, or that decodes to blank, is ignored —
|
|
if the widget does not appear, check that the value round-trips through
|
|
`base64 -d`. A present `PAPERCLIP_CLOUD_UI_SNIPPET` always wins, blank
|
|
included: clearing the plain variable to blank disables injection even while
|
|
a base64 value is still deployed. Everything else about the snippet is
|
|
unchanged.
|
|
|
|
Base64 does not defeat every firewall. Some decode the value before matching,
|
|
so they reject a base64 snippet whose decoded bytes still contain script
|
|
markup. Deliver a bare script body (below) through one of these.
|
|
|
|
## Bare script body
|
|
|
|
Set the value to the script body alone — the JavaScript with no surrounding
|
|
`<script>` element:
|
|
|
|
```sh
|
|
PAPERCLIP_CLOUD_UI_SNIPPET_B64="$(base64 < snippet.body.js)"
|
|
```
|
|
|
|
The server wraps a bare body in a `<script>` element before it inserts it. The
|
|
first non-whitespace character decides the form: a value that starts with `<`
|
|
is treated as markup and injected unchanged; any other value is treated as a
|
|
body and wrapped. This applies to both the plain and the base64 variant.
|
|
|
|
The body must be safe to embed inline. It must not contain a literal
|
|
`</script>`, which would close the wrapper early. Because the value carries no
|
|
`<script` marker, a firewall that decodes base64 before matching passes it.
|
|
|
|
## Plain closed beta
|
|
|
|
Set the value to this standard embed, replacing `YOUR_CHAT_APP_ID` with the
|
|
public chat app ID for the target environment:
|
|
|
|
```html
|
|
<script>
|
|
(function(d) {
|
|
var script = d.createElement('script');
|
|
script.src = 'https://chat.cdn-plain.com/index.js';
|
|
script.onload = function() { Plain.init({ appId: 'YOUR_CHAT_APP_ID' }); };
|
|
d.head.appendChild(script);
|
|
})(document);
|
|
</script>
|
|
```
|
|
|
|
No signing secret or Plain API key is required. No Paperclip customer identity
|
|
or organization data is passed. Plain manages the anonymous browser session;
|
|
there is no Paperclip account-switch integration. Ask users for identifying
|
|
information when needed. The existing feedback flag remains unchanged.
|
|
|
|
Docs: [Plain chat](https://www.plain.com/docs/product/channels/chat).
|
|
|
|
## Verification and rollback
|
|
|
|
On staging, open `/`, `/index.html`, and an organization dashboard directly.
|
|
Confirm the bubble appears and a test message reaches Plain. Verify the support
|
|
reply returns. On a self-hosted instance, confirm no snippet or widget is loaded.
|
|
Unset the snippet and restart to remove it on the next page load. Existing open
|
|
tabs retain the widget until refreshed. No production deployment is implied.
|