App risk review
Apps in tailrmade are graphs, and a graph can do real things: call APIs, spend your AI allowance, read and write your saved data, reach your computer through the Companion. Before an app you did not build runs, tailrmade reads its nodes and shows you what it is going to need.
That list is the Access panel. This page explains what is in it and how it is worked out.
The one distinction that matters
Most of what an app does can be worked out in advance. If a node calls an API, tailrmade can tell you which address. If a node uses a saved key, it can name the key. You can read the list and decide.
Two things get in the way of that.
A value can be set while running — a URL that arrives from another node rather than being typed in. The action is still known (it makes a request), but the address is not, so there is nothing to show you.
And some code can't be checked in advance. A custom function set to Main Thread, an HTML, IFrame, Embed or Slideshow node with Sanitize input switched off, or a Load NPM package node, runs as ordinary JavaScript in the page. It decides what to do as it goes. A package's code runs as soon as it is imported, so Load NPM counts as unsandboxed even when the function that uses the package does not.
The levels are named after how well you can judge an app, because that is the only part of this that forms a scale:
| Level | Meaning |
|---|---|
| Unrestricted | Runs code outside the sandbox. Tailrmade can't check what that code does. |
| Dynamic | Everything it can do is listed, but some addresses or values are set while the app runs. |
| Static | Everything it can do is listed, with fixed addresses and values. |
| Static · loads only | Only loads content from the sites listed. |
| No access | Needs no access. |
An app's headline level is the highest level any single node reaches — not an average. One unsandboxed node makes the whole app Unrestricted, even if everything else is tame. The counts beside the headline show how the app's nodes spread across the levels, so you can see whether that node is the exception or the rule.
Note what the scale does not tell you: how much is at stake. A Static app can still spend your API credits and rewrite your saved data — you can simply see exactly that it will. That part is carried by the impact tags on each action, not by the level.
What "Unrestricted" can and cannot do
Unsandboxed code runs with the same reach as tailrmade itself. It can:
- read, change or delete data you have saved in tailrmade;
- use your saved API keys, and anything they pay for;
- send requests through your Companion, including to your local network;
- contact any site.
It cannot:
- read files on your computer or touch other applications;
- use your camera, microphone or location, unless you have already granted tailrmade that permission in your browser.
This is why the panel stops being useful at this level and trust takes over. Check who the app came from — the publisher is shown at the top of the panel, as Published by with a Tailrmade badge when the app was published by us. Apps opened from a file or this device show Unknown publisher: no account vouches for them, including ones you built yourself.
What an app can do
The Access tab lists each thing the app's nodes can do, under This app can:. Cards carry impact tags ("Can cost money", "Can send your data out") so you do not have to infer the cost from the description. Impact is a set, not a ranking: money, your saved data, your computer and your privacy are different things, not degrees of one thing.
| This app can… | Level | What it means |
|---|---|---|
| Run unsandboxed code | Unrestricted | Arbitrary JavaScript in the page; Tailrmade can't check it in advance |
| Send data to other sites | Static / Dynamic | Can send app data to the listed addresses and use their replies |
| Use API keys | Static / Dynamic | Can send the named saved key with its requests |
| Use your Companion | Static | Requests go through the Companion, which reaches your computer |
| Change Tailrmade data | Static | Can add, change or delete data saved in tailrmade |
| Read Tailrmade data | Static | Can read data saved in tailrmade, including your other apps' data |
| Use AI services | Static | Can send prompts and images to your AI provider; uses your allowance |
| Open web pages | Static / Dynamic | Opens a new tab; anything in an address set while running reaches it |
| Load images from addresses set while running | Dynamic | Another node picks the address, so app data can travel inside it |
| Load data from other sites | Static · loads only | A plain GET to the listed addresses; each site sees your IP address |
| Load content from other sites | Static · loads only | Links and embeds in sanitized HTML; each site sees your IP address |
| Load images from other sites | Static · loads only | Each site sees your IP address when the image loads |
The actions marked Static / Dynamic land on whichever applies: Static when the address or key name is constant, Dynamic when it arrives from another node.
Each card lists the destinations it applies to and the nodes that need it. Session storage is temporary state for the currently loaded app and reports no storage access risk. Loading or clearing an app resets that state. Storage access disclosures apply to saved data in IndexedDB or Cloud storage. Where a destination cannot be worked out ahead of time, it reads "Chosen while the app runs" and the action counts as Dynamic.
How it is worked out
tailrmade resolves input values without executing the graph. Constants, labels and string concatenation are followed, so a URL assembled from fixed parts is shown as a real address. An input fed by something it cannot resolve — a widget, a value read out of JSON, a custom function — is treated as dynamic, and the destination stays unknown.
Addresses are shown without credentials or fragments. Query parameter names
are shown and their values are masked, as in
https://example.com/search?q=…&key=…: the query is part of what reaches the
site, so hiding it entirely would have made the review quieter than the truth,
while the values stay out so a secret typed into an address is never printed
back at you.
What runs while you are still reading
What is allowed to run is decided by impact, not by level. A node whose only impact is Can reveal your visit keeps running — it was already safe. Everything with money, your data or your computer at stake is paused, however easy or hard it is to judge. So a fully Static storage write waits for you, and a Static · loads only image does not. Execution stops a branch when it reaches a paused node; other branches carry on. Approving the app resumes the stopped chains where they stopped, without rerunning what already finished.
Approval is remembered per app version. Editing the app's nodes or wiring produces a new version, so it is reviewed again.
Lowering your own app's level
If you publish apps, every level above No access is something you are asking your users to accept. Most Unrestricted apps do not need to be:
- Leave Sanitize input on for HTML, IFrame and Slideshow nodes. You only need
it off for intermediate template nodes whose output feeds another node, or for
markup that genuinely needs
<style>blocks or inline handlers. - Keep custom functions on Worker Thread, the default. The QuickJS sandbox has no network and no browser APIs, so a worker function reports no action at all. See Create custom nodes.
- Use real nodes instead of code. An HTTP node reports its address; the same
request made with
fetchinside a main-thread function reports nothing and forces the whole app to Unrestricted. - Keep addresses static where you can. A URL built from constants is shown to your users and reports as Static; one that arrives from another node reports as Dynamic and shows them nothing.
Reading access from inside an app
The Tailrmade node has an Output app access trigger that returns the same data as the Access panel:
{
app: 'My app',
source: { ownerId: '…', owner: { name: '…', email: '…' }, verified: false },
requiredLevel: 'static',
actions: [
{
kind: 'api-key',
title: 'Use API keys',
description: 'Can send the saved key below with its requests.',
level: 'static',
impact: ['Can cost money', 'Can send your data out'],
target: 'OPENAI',
resolved: true,
nodes: [{ id: '…', name: 'HTTP' }],
},
],
}
level and requiredLevel are one of unrestricted, dynamic, static,
loads-only or none. resolved is false when the value is only chosen while
the app runs, which is what puts an action at dynamic.
Use it to show your own users what the app needs, or to assert in a test that an app has not quietly picked up a new one.