tunSAFE
Tún: portable single-file dashboards for SharePoint and OneDrive
Overview
From the repository's own README, as read at the audited commit. Badges and raw HTML are left out.
Interactive dashboards that are files, not services. Built as single self-contained HTML artefacts, distributed through SharePoint and OneDrive, openable by anyone who can open the folder, with no Power BI licence, no workspace, no capacity and no admin approval.
Tún is a passion project, credited to the product owner and the maintainer. <3 The idea, the name and the direction are the product owner's; deliberately open-ended. The maintainer builds the plumbing: the awkward platform constraints measured so nobody has to rediscover them, a working template, the gates that keep it honest, and a set of RAIWork skills so building and sharing an artefact is a sentence rather than a project. Scaffolding, not a specification. Nobody asked for this and nothing depends on it. That is the point: it exists because sharing a number with a colleague should not cost a licence, a workspace and three weeks, and because finding out exactly why it does was interesting enough to keep going.
The idea in one paragraph
Building a dashboard is easy. Sharing one is where the organisation loses weeks. It takes licences per viewer, a workspace, a capacity, a publish step, and still no way to show it to someone without a Pro seat. So people fall back to a screenshot in PowerPoint, which is dead on arrival. Instead: make the dashboard a document. One .html file with the data, charts and interaction logic inside it. Drop it in a SharePoint library. Everyone already knows how to share a document, and SharePoint already handles permissions, versioning, audit, sync and offline. Reuse that machinery instead of fighting it.
Status
141b38605b8bOBSERVED · 2026-10-10Install
Commands as the repository documents them. They are shown, not run.
uv sync --extra verify && uv run playwright install chromium
uv run --no-project --no-config --index-url https://pypi.org/simple --with playwright python tools/verify_dashboard.py <fil
uv run --no-project --no-config --index-url https://pypi.org/simple --with playwright python tests/test_feedback_link_brows
uv sync --extra verify && uv run playwright install chromium # once
uv run python tools/verify_dashboard.py /tmp/demo.html
git clone <your-forge>/<owner>/tun.git
Host compatibility
What the documentation claims. We have not run a compatibility test.
| Host | Status | Notes |
|---|---|---|
| claude-code | mentioned | |
| codex | mentioned | |
| copilot | mentioned | |
| cursor | mentioned | |
| gemini-cli | mentioned |
What it tells the agent
The instruction file, verbatim from the audited commit — this is the text the model reads, and the surface the audit's instruction layer examines. Quoted here so you can judge it without cloning anything.
---
name: tun-data
description: Refresh the data in an existing standalone dashboard, or add a new data source to it. Converts Excel to a usable format, re-extracts from the source, and rebuilds the file in place so existing share links keep working. Use when dashboard numbers are stale, someone asks to update or refresh a dashboard, or wants to point it at different data.
license: AGPL-3.0-or-later
---
# Refresh or repoint a dashboard's data
**Check first whether the user even needs you.** A paired dashboard can be updated with no
tooling at all, and if that fits, say so instead of doing it for them — it makes them
self-sufficient, which is worth more than one refresh.
## The no-tooling routes (offer these first)
**A — drag in, click, drag out.** No text editing, no terminal.
1. Open the dashboard and **drag a CSV onto it** — it re-renders immediately with the new
numbers, and the header says *"ikke lagret ennå"* so nobody mistakes it for published.
2. Click **"Lagre datafil (data.js)"** — the dashboard writes the data file for you.
3. Drop that `data.js` into the SharePoint folder next to the dashboard.
Everyone with the folder synced sees the new numbers next time they open it. Norwegian
number formats (`1 500 000,50`) are parsed correctly, and a column only becomes numeric if
every value in it parses — so an ID column survives as text.
**B — edit `data.js` in Notepad.** For a small hand-maintained table. The file can carry
raw CSV between backticks:
```js
window.DASHBOARD_CSV = `Maaned,Region,Belop
2026-01,Oslo,1500000
2026-02,Bergen,2500000`;
window.DASHBOARD_META = {generated:"2026-03-01", source:"manuelt vedlikeholdt", rows:2};
```
Paste the new CSV between the backticks, save. That's the whole update.
## When you should do it (route C)
Take over when the refresh involves real work: re-running a query, joining sources,
aggregating to a coarser grain, or converting from Excel. Then continue below.
## Step 1 — Find out what it was built from
Read the provenance block at the top of the dashboard HTML:
```bash
head -30 <dashboard.html>
```
It records the source description, extract timestamp, build timestamp, requester, and
the row count. That tells you what to re-extract and what the numbers should roughly
look like afterwards.
If the provenance block is missing, the dashboard predates the convention or was
hand-edited. Reconstruct what you can, ask the user, and **make sure the rebuild has a
proper one.**
## Step 2 — Get fresh data in the same shape
The rebuild only works if the new extract has the **same column names**. If columns
were renamed or dropped upstream, the build will fail its own checks — that is the
intended behaviour, not a bug to work around. Tell the user what changed.
**Excel (`.xlsx`)** — convert to CSV. Prefer stdlib-free options in this order:
```bash
# 1. if the file is simple, ask the user to "Save As CSV" — zero tooling, zero risk
# 2. otherwise, a converter (only if already available):
python3 -c "
import openpyxl, csv, sys
wb = openpyxl.load_workbook(sys.argv[1], data_only=True) # data_only: values, not formulas
ws = wb[sys.argv[3]] if len(sys.argv) > 3 else wb.active
with open(sys.argv[2], 'w', newline='', encoding='utf-8') as f:
csv.writer(f).writerows(ws.values)
" input.xlsx output.csv [SheetName]
```
⚠️ `data_only=True` matters — without it you get formula strings instead of values and
every numeric column silently becomes text. If openpyxl is not installed, do not
install a toolchain on the user's behalf for a one-off; ask them to Save As CSV.
**A query (Snowflake etc.)** — re-run it and write the result to CSV. Record the query
text in `--source` so the next refresh is reproducible.
**Sanity-check the extract before rebuilding.** Row count in the same ballpark as last
time? If it dropped by 90%, something upstream broke — say so instead of shipping a
dashboard that quietly shows a tenth of the business.
## Step 3 — Rebuild in place
Same output path, same column arguments as the original build:
```bash
python3 tools/build_dashboard.py \
--data <fresh.csv> \
--title "<same title>" \
--dimension <same> --measure <same> --time <same> \
--filters <same> \
--source "<source description>" \
--requested-by "<original requester>" \
--out <same/path/dashboard.html>
```
Overwriting the same path is deliberate: the share link keeps working and SharePoint
keeps the old version in its version history.
## Step 4 — Verify, then compare
```bash
python3 tools/verify_dashboard.py <dashboard.html>
```
Then do the thing that catches real errors: **compare the new headline number to the
old one** and say it out loud.
> "Total Belop went from 648 924 441 to 701 233 908 — up 8%, and there are 40 more rows.
> That looks like a normal month."
If a number moved implausibly, investigate before publishing. This comparison is the
main defence against a broken extract sailing through every automated check.
## Step 5 — Republish
Copy over the published copy (`tun-publish`), same path. Then tell the user what
the new "as of" date is, because that is the only thing that visibly changed.
## Adding a new data source
To combine sources, join them into one flat table **before** building — the template
takes a single array of rows. Do the join in Python or SQL, keep the joined extract, and
describe both sources in `--source`.
If the two sources have different grains, aggregate to the coarser one first. Silently
fanning out rows through a join is the fastest way to produce a dashboard that
double-counts.
## Automating a recurring refresh
If a dashboard needs refreshing on a schedule, that is a small pipeline: extract →
build → verify → copy to the synced folder. Only automate it once the manual path has
run cleanly a few times, and make sure a failure is **loud** — a silently stale
dashboard is worse than an obviously broken one, because people keep trusting it.Trust audit
SAFEgrade B · trust 89/100 Nothing in the source contradicts what it says it does. Grade A is reserved for packages that have also passed the behavioural sandbox.
| Layer | What it checks | Result |
|---|---|---|
| L0 | Provenance & inventory | PASS |
| L1 | Static analysis of the code | PASS |
| L2 | Instruction surface (what it tells the agent) | PASS |
| L3 | Class-specific surface | PASS |
| L4 | Behavioural (sandbox) | SKIPPED |
What the source does
- Filesystem
- declared (1 observation(s))
- Network
- declared (4 observation(s))
- Shell
- none-observed
- Dependencies
- pinned
- Secrets in source
- none-found
Findings (12)
exec(SHIM.split("gzip.compress = older_build")[0], ns) # defines older_build onlyexec(SOURCE[:start] + mutated + SOURCE[end:], ns) # noqa: S102 — test harness
print("next: python3 ../../tools/verify_standalone_core.py " + str(OUT))print("next: python3 ../../tools/verify_standalone_core.py " + str(OUT))print("next: python3 ../../tools/verify_standalone_core.py " + str(OUT))print("next: python3 ../../tools/verify_standalone_core.py " + str(OUT))<script id="cube" type="application/gzip;base64">H4sIAAAAAAAC/xyaZVRUbduGDwwUYbqHoTsM7G6xu5vHQsTAJAwEBBSRVkCk7EBRTFBRbBQDu7u7O777/f45MOx9xVnXWqb0UpK6SM6hDwpOPFfSxVxJ4Twlv/LkXCrWMMNSwruPMtxjlawJ1PKtWMa
WANT_HEADER = bytes.fromhex("1f8b08000000000002ff")return base64.b64decode(m.group(1))
b64decode( ... subprocess.
description: "Run an adversarial review of a PR, branch, or large diff through two senior-engineer lenses — Magnus (correctness: falsify the author's story, trace every finding through the actual code
When the user says "Magnus style review", "Magnus-style review", "run a Magnus review", "adversarial review", or asks for a senior/skeptical review of a PR, branch, or large diff that goes beyond veri
Gates applied: no_behavioural_pass.
141b38605b8bfull audit observations/trust-audit/skill/jkorsvik__tun.json · Report an issue / request a re-scanAudit history
Every audit this skill has had.
| Date | Source | Verdict | Grade | Score | Change |
|---|---|---|---|---|---|
| 2026-10-10 | 141b38605b8b | SAFE | B | 89 | first audit |
Questions
What does the tun skill do?
Tún: portable single-file dashboards for SharePoint and OneDrive
Is tun safe to install?
The audit found nothing in the source that contradicts what it says it does, and graded it B (89/100). Grade A is held back for packages that have also passed a sandboxed behavioural run, which is why a clean skill reads B.
What can tun access on my machine?
The audit observed that it reaches the network and reads or writes files. Each of those is consistent with what it says it does. Secrets in the source: none found.
Which assistants does tun work with?
Its documentation mentions claude-code, codex, copilot, cursor and gemini-cli. That is what the text claims, not a compatibility test we ran.
How current is this page?
The grade is for one exact copy of the source (141b38605b8b), read on 2026-10-10. The repository is watched, and a new audit runs when it changes — this is the first audit.