@passioncode-ai/fabric-agent-adapter 0.3.1 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +52 -0
- package/README.md +41 -5
- package/SECURITY.md +14 -0
- package/bin/fabric-agent-adapter.js +87 -43
- package/package.json +10 -5
- package/plugins/fabric-agent-adapter/.claude-plugin/plugin.json +2 -3
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/SKILL.md +1 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +240 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard.md +32 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/events-and-notifications.md +44 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/lifecycle.md +63 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/migrating-a-service.md +32 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/protocol.md +83 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/surfaces-and-auth.md +58 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service.py +373 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-service.mjs +380 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_service.py +663 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/sample_service.py +226 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +8 -1
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Dashboard principles
|
|
2
|
+
|
|
3
|
+
The dashboard is where the operator decides; it must stay truthful when parts of the
|
|
4
|
+
service are not.
|
|
5
|
+
|
|
6
|
+
1. **Same origin, static assets.** HTML shell plus hashed `app.js`/`app.css` from a
|
|
7
|
+
whitelist; no inline script; a strict CSP. Nothing from a CDN — the page must work
|
|
8
|
+
offline on a plane.
|
|
9
|
+
2. **State first, then detail.** The top of every page answers "is it working, and
|
|
10
|
+
does it need me?" — the same `status`, `degraded` and attention tiles the
|
|
11
|
+
well-known document publishes, so the page and Fabric Dashboards never disagree.
|
|
12
|
+
3. **Calm refresh.** Poll the page's own API every 5 s while visible, back off when
|
|
13
|
+
hidden, and pause while a field is focused, a form has unsaved input or a dialog is
|
|
14
|
+
open. Replace a region only when its data changed; keep focus, scroll and open
|
|
15
|
+
sections.
|
|
16
|
+
4. **Partial failure stays partial.** Load panels independently (`Promise.allSettled`);
|
|
17
|
+
a failed panel shows its own one-sentence error and a retry, and the rest render.
|
|
18
|
+
5. **Every error is a sentence and an action.** "Store listing push failed: App Store
|
|
19
|
+
Connect rejected the key. Settings → Keys" — never a status code, a stack trace or a
|
|
20
|
+
blank area. Show when data was last fresh.
|
|
21
|
+
6. **Server down is a state, not a crash.** When the API stops answering, keep the
|
|
22
|
+
last data greyed with "Service not answering since 17:02", and recover by itself
|
|
23
|
+
when it returns.
|
|
24
|
+
7. **Approvals show the diff and its hash.** Any write the operator approves shows
|
|
25
|
+
before → after and the hash the service will verify at apply time.
|
|
26
|
+
8. **Theme.** Follow the operator's product design system (PassionCode.ai tokens for
|
|
27
|
+
Fabric-family services); respect `prefers-color-scheme` unless the product fixes a
|
|
28
|
+
theme; never encode meaning by colour alone.
|
|
29
|
+
9. **Links into the page are stable.** Event `link`s point at hash routes that survive
|
|
30
|
+
a reload (`/dashboard#/approvals/77`) so a notification opens the exact item.
|
|
31
|
+
10. **No second browser tab required.** With Fabric Dashboards installed, the page is
|
|
32
|
+
shown inside it; do not open windows or tabs on your own — use in-page dialogs.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Events and notifications
|
|
2
|
+
|
|
3
|
+
## Map the log you already keep
|
|
4
|
+
|
|
5
|
+
| Existing log | View |
|
|
6
|
+
|---|---|
|
|
7
|
+
| a SQL `events`/`jobs` table with an autoincrement id | `SELECT … WHERE id > ? ORDER BY id LIMIT ?`; `id` as string |
|
|
8
|
+
| per-job JSONL files | keep one append-only `activity.jsonl` index written beside them, or merge by `(ts, job, seq)` into a composite id `"<ts>-<job>-<seq>"` that still sorts |
|
|
9
|
+
| a journal with no id | add a monotonic counter when appending; backfill ids once on migration |
|
|
10
|
+
| nothing | `JsonlEventLog` from the kit |
|
|
11
|
+
|
|
12
|
+
The feed is a view: never copy rows into a second store that can drift from the first.
|
|
13
|
+
|
|
14
|
+
## Writing an event
|
|
15
|
+
|
|
16
|
+
- `kind`: dotted, stable, past tense or state (`job.failed`, `job.awaiting_choice`,
|
|
17
|
+
`service.started`, `collector.stale`). Hosts filter on it.
|
|
18
|
+
- `level`: `info` (routine), `notice` (the operator may want to look), `warning`
|
|
19
|
+
(something is degrading), `error` (something failed and needs action).
|
|
20
|
+
- `text`: one sentence, in the operator's language, with the object named and the
|
|
21
|
+
number stated — "The Q3 report draft is ready for your approval (12 sections, 3
|
|
22
|
+
languages)." Never a machine id, never a stack trace.
|
|
23
|
+
- `subject`: what it is about (`{type: "report", id: "q3", label: "Q3 report"}`), so a host
|
|
24
|
+
can group.
|
|
25
|
+
- `link`: the page that resolves it.
|
|
26
|
+
|
|
27
|
+
## When to set `notify: true`
|
|
28
|
+
|
|
29
|
+
Notify when the operator must act (an approval is waiting, a key expired, a job failed
|
|
30
|
+
after retries) or asked to be told (a long job finished). Do not notify for routine
|
|
31
|
+
progress, retries that will succeed, or anything that fires more than a few times an
|
|
32
|
+
hour. The host decides whether to show it — quiet hours, per-service settings — and
|
|
33
|
+
debounces; the service decides only what is notification-worthy.
|
|
34
|
+
|
|
35
|
+
## Lifecycle events every service emits
|
|
36
|
+
|
|
37
|
+
`service.started` (with the build), `service.stopping`, `service.degraded` and
|
|
38
|
+
`service.recovered` when a degraded source appears or clears, `update.available` when
|
|
39
|
+
the service learns of a newer release.
|
|
40
|
+
|
|
41
|
+
## Retention
|
|
42
|
+
|
|
43
|
+
At least seven days or 1000 events, whichever is more; trim oldest first; a cursor
|
|
44
|
+
older than retention returns the oldest retained page, not an error.
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Lifecycle — supervisor, install, release layout, state
|
|
2
|
+
|
|
3
|
+
## launchd plist (generate it with `launchd_plist`)
|
|
4
|
+
|
|
5
|
+
| Key | Value | Why |
|
|
6
|
+
|---|---|---|
|
|
7
|
+
| `Label` | reverse-DNS, stable (`com.example.example-agent`) | the host controls the job by label |
|
|
8
|
+
| `ProgramArguments` | absolute interpreter + module/script inside a **release** directory | a worktree or checkout path breaks the day it moves |
|
|
9
|
+
| `RunAtLoad` | `true` | back at every login |
|
|
10
|
+
| `KeepAlive` | `true` | `{SuccessfulExit:false}` leaves a cleanly exited service down |
|
|
11
|
+
| `ThrottleInterval` | `10` | bounds a crash loop |
|
|
12
|
+
| `ExitTimeOut` | drain time + margin (40 s default) | SIGKILL arrives after it |
|
|
13
|
+
| `ProcessType` | `Background` | scheduler hint |
|
|
14
|
+
| `EnvironmentVariables` | `PATH` set explicitly; `*_FILE` paths to secrets | never a secret value — the plist is world-readable in backups |
|
|
15
|
+
| `StandardOutPath`/`StandardErrorPath` | `~/Library/Logs/<id>/service.log` | one place the host tails |
|
|
16
|
+
|
|
17
|
+
## Install, upgrade, uninstall
|
|
18
|
+
|
|
19
|
+
1. Build or unpack the release into `~/.local/share/<id>/releases/<version>-<sha12>/`
|
|
20
|
+
(a venv or `node_modules` inside it). Never edit a release after it is written.
|
|
21
|
+
2. `write_descriptor` (port and id claims are checked here).
|
|
22
|
+
3. `launchd_install`: write + `plutil -lint`, `enable`, `bootout` and wait until
|
|
23
|
+
`launchctl print gui/<uid>/<label>` fails, `bootstrap` with up to five retries,
|
|
24
|
+
then poll the well-known document until it answers with this `id.instance`.
|
|
25
|
+
4. Upgrade = new release, rewrite the plist with the new path, `launchd_install`
|
|
26
|
+
again. Keep the previous release for rollback; prune older ones by count.
|
|
27
|
+
5. Uninstall = `launchd_uninstall` + `remove_descriptor`. Data stays.
|
|
28
|
+
|
|
29
|
+
A host stops a service with `bootout` **and** `disable` (off survives a login) and
|
|
30
|
+
starts it with `enable` + `bootstrap`; restart is `kickstart -k`. The service never
|
|
31
|
+
implements these verbs itself — its CLI calls launchctl the same way.
|
|
32
|
+
|
|
33
|
+
## Directories
|
|
34
|
+
|
|
35
|
+
| What | macOS | Linux |
|
|
36
|
+
|---|---|---|
|
|
37
|
+
| data + config | `~/Library/Application Support/<id>/` | `${XDG_DATA_HOME:-~/.local/share}/<id>/` |
|
|
38
|
+
| logs | `~/Library/Logs/<id>/` | `${XDG_STATE_HOME:-~/.local/state}/<id>/logs/` |
|
|
39
|
+
| cache (deletable any time) | `~/Library/Caches/<id>/` | `${XDG_CACHE_HOME:-~/.cache}/<id>/` |
|
|
40
|
+
| code | `~/.local/share/<id>/releases/…` | same |
|
|
41
|
+
|
|
42
|
+
A repository that versions the data itself (a registry, a plan) may hold the data; the
|
|
43
|
+
service's own code checkout and its releases may not. Directories 0700, secret files 0600. A store carries a schema version and the service
|
|
44
|
+
refuses to open a store newer than its code. The cache holds only what can be rebuilt;
|
|
45
|
+
anything the operator would miss belongs in data.
|
|
46
|
+
|
|
47
|
+
## Logs
|
|
48
|
+
|
|
49
|
+
One structured log (JSON lines) rotated by size (5 × 5 MB is a sane default) plus the
|
|
50
|
+
launchd stdout file, truncated at start. Configure logging once, in the entry point.
|
|
51
|
+
Never log a token, a cookie, a login code or a request body that may carry one.
|
|
52
|
+
|
|
53
|
+
## Shutdown
|
|
54
|
+
|
|
55
|
+
On `SIGTERM`: stop accepting new work, append a `service.stopping` event, drain
|
|
56
|
+
in-flight work until `ExitTimeOut` minus a margin, persist what was interrupted so the
|
|
57
|
+
next start resumes it, release the lock, exit 0.
|
|
58
|
+
|
|
59
|
+
## Linux
|
|
60
|
+
|
|
61
|
+
Until a systemd adapter exists, declare `lifecycle.manager: "none"`. A `systemd --user`
|
|
62
|
+
unit with `Restart=always` gives the same guarantees; the host will control it once the
|
|
63
|
+
adapter ships. Everything else in this skill applies unchanged.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Migrating an existing service
|
|
2
|
+
|
|
3
|
+
Order matters: each step leaves the service answering.
|
|
4
|
+
|
|
5
|
+
1. **Measure first.** Run `check_service.py --descriptor <draft>` against a descriptor
|
|
6
|
+
written by hand into a temporary `--services-dir`; keep the `FAIL` list as the
|
|
7
|
+
migration's checklist.
|
|
8
|
+
2. **Add the well-known route and the build identity.** Read-only, no risk.
|
|
9
|
+
3. **Add the events view** over the existing log, then the login-code routes. Keep the
|
|
10
|
+
service's old login path until the operator has switched.
|
|
11
|
+
4. **Move the lock to the top of startup** — before the startup hook that resumes jobs.
|
|
12
|
+
Test: a second copy exits 75 and the first copy's running jobs are untouched.
|
|
13
|
+
5. **Move state out of the code tree** if it lives there: copy to the OS data
|
|
14
|
+
directory, switch the paths, keep the old tree read-only for one release, then
|
|
15
|
+
delete it with the operator's consent.
|
|
16
|
+
6. **Regenerate the plist** from `launchd_plist`, pointing at a release; delete stray
|
|
17
|
+
`.plist.bak` files from `~/Library/LaunchAgents` after moving them elsewhere.
|
|
18
|
+
7. **Write the descriptor** from the installer; resolve any port claim it refuses by
|
|
19
|
+
moving the newcomer, never the established service.
|
|
20
|
+
8. **Re-run the probe** until nothing `FAIL`s; record the table in the service's
|
|
21
|
+
verification document.
|
|
22
|
+
|
|
23
|
+
## Gaps that existing services usually have
|
|
24
|
+
|
|
25
|
+
| Gap | What fixes it |
|
|
26
|
+
|---|---|
|
|
27
|
+
| A health route that answers `{"ok": true}` with no identity | the well-known document with `service.id`, `instance`, build and pid |
|
|
28
|
+
| The port bind treated as the single-instance guarantee, with a startup hook that runs first | the lock at the top of startup; a second copy exits 75 |
|
|
29
|
+
| Data, logs or caches inside the repository | the OS data, log and cache directories; the old tree read-only for one release |
|
|
30
|
+
| `KeepAlive {SuccessfulExit:false}`, a plist pointing at a checkout or a worktree | a plist generated by `launchd_plist`, pointing at a release |
|
|
31
|
+
| A port another service already uses by default | the port claim at install; move the newcomer |
|
|
32
|
+
| An event log in a private shape | the events view over it, with one sentence per event |
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# fabric-service/0.1 — wire reference
|
|
2
|
+
|
|
3
|
+
Pinned to `fabric-agent-contract` commit `a5a27092ba0dcc5facfbeae8b359146dfb403e9a`
|
|
4
|
+
(`docs/specification/service.md`, DEC-0015). The contract's schemas are normative;
|
|
5
|
+
this page is the working summary. Extension key:
|
|
6
|
+
`https://fabric.passioncode.ai/agent-contract/extensions/service/0.1`.
|
|
7
|
+
|
|
8
|
+
## Descriptor — written by the installer
|
|
9
|
+
|
|
10
|
+
Directory: macOS `~/Library/Application Support/ai.passioncode.fabric/services/`,
|
|
11
|
+
Linux `${XDG_DATA_HOME:-~/.local/share}/passioncode-fabric/services/`, or
|
|
12
|
+
`FABRIC_SERVICES_DIR`. File `<id>.<instance>.json`, mode 0600, atomic.
|
|
13
|
+
|
|
14
|
+
| Field | Rule |
|
|
15
|
+
|---|---|
|
|
16
|
+
| `protocol` | `"fabric-service/0.1"` |
|
|
17
|
+
| `id`, `instance` | `^[a-z][a-z0-9-]{1,62}$`, `^[a-z][a-z0-9-]{0,31}$` (default `default`); the pair is unique per machine |
|
|
18
|
+
| `name`, `summary` | ≤ 80 and ≤ 200 characters |
|
|
19
|
+
| `origin` | `http://127.0.0.1:<port>`; the port is a machine-wide claim |
|
|
20
|
+
| `auth` | `tokenFile` (0600); `header` default `Authorization` with `scheme` `Bearer`; any other header carries the raw token with `scheme: "none"` |
|
|
21
|
+
| `lifecycle` | `manager` `launchd` (then `label` and `plist`) or `none` |
|
|
22
|
+
| `paths` | `data` (required), `logs[]`, optional `config`, `cache` |
|
|
23
|
+
| `commands` | only `doctor` and `update`, argument arrays whose first item is an absolute or `~/` path |
|
|
24
|
+
| `source.repository`, `fabricManifest` | optional |
|
|
25
|
+
| `installedAt`, `installedBy` | when and by which installer version |
|
|
26
|
+
|
|
27
|
+
## Well-known document — `GET /.well-known/fabric-service`
|
|
28
|
+
|
|
29
|
+
No auth; the Host/Origin guard applies; answered from memory in under 100 ms.
|
|
30
|
+
|
|
31
|
+
```json
|
|
32
|
+
{
|
|
33
|
+
"protocol": "fabric-service/0.1",
|
|
34
|
+
"service": { "id": "example-agent", "instance": "default", "name": "Example Agent",
|
|
35
|
+
"version": "0.2.0", "build": { "commit": "8b80be9", "dirty": false, "builtAt": "2026-09-28T17:40:00Z" } },
|
|
36
|
+
"process": { "pid": 58090, "startedAt": "2026-09-28T17:41:02Z" },
|
|
37
|
+
"status": "degraded",
|
|
38
|
+
"degraded": [{ "source": "llm", "reason": "No model key: drafting is paused." }],
|
|
39
|
+
"summary": [{ "label": "Jobs running", "value": 2 }, { "label": "Awaiting you", "value": 1, "attention": true }],
|
|
40
|
+
"surfaces": { "dashboard": { "path": "/dashboard", "login": true },
|
|
41
|
+
"mcp": { "path": "/mcp", "transport": "streamable-http" },
|
|
42
|
+
"events": { "path": "/fabric/v1/events" } },
|
|
43
|
+
"update": { "available": null }
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
- `status`: `starting | ready | degraded | stopping`. No answer means `down`.
|
|
48
|
+
- `build` needs `commit` or `digest` (`sha256:<64 hex>`).
|
|
49
|
+
- `summary`: at most six tiles; `attention: true` counts toward the host's badge.
|
|
50
|
+
- `surfaces.events` is required; `dashboard` and `mcp` when the service has them.
|
|
51
|
+
|
|
52
|
+
## Events page — `GET /fabric/v1/events?after=<cursor>&limit=<n>`
|
|
53
|
+
|
|
54
|
+
Token required. `limit` default 50, maximum 200. Without `after`: the newest `limit`,
|
|
55
|
+
ascending. `cursor` is the last id returned, or the `after` you sent when the page is
|
|
56
|
+
empty, or `null` for an empty log. Retain at least seven days or 1000 events.
|
|
57
|
+
|
|
58
|
+
```json
|
|
59
|
+
{ "events": [ { "id": "4213", "at": "2026-09-28T17:55:10Z", "kind": "job.awaiting_choice",
|
|
60
|
+
"level": "notice", "text": "The Q3 report draft is ready for your approval.",
|
|
61
|
+
"subject": { "type": "report", "id": "q3", "label": "Q3 report" },
|
|
62
|
+
"link": "/dashboard#/approvals/77", "notify": true } ],
|
|
63
|
+
"cursor": "4213" }
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
`kind` is dotted lowercase; `level` is `info | notice | warning | error`; `link` is a
|
|
67
|
+
path on the service origin, never an absolute URL. Optional SSE stream at
|
|
68
|
+
`surfaces.events.stream` carrying the same objects.
|
|
69
|
+
|
|
70
|
+
## Login code — `POST /fabric/v1/login-code`
|
|
71
|
+
|
|
72
|
+
Token required → `{ "url": "/fabric/v1/login?code=<16–256 url-safe chars>", "expiresAt": "…" }`,
|
|
73
|
+
single use, at most 120 s, recorded as used before it is honoured. `GET` that URL →
|
|
74
|
+
`Set-Cookie` `HttpOnly; SameSite=Strict`, `302` to `surfaces.dashboard.path`.
|
|
75
|
+
|
|
76
|
+
## Semantic rules a host applies
|
|
77
|
+
|
|
78
|
+
| Code | Rule |
|
|
79
|
+
|---|---|
|
|
80
|
+
| `FAC-SEM-009` | well-known `service.id.instance` equals the descriptor's, else the port holder is `foreign` |
|
|
81
|
+
| `FAC-SEM-010` | no two descriptors claim one port or one `id.instance` |
|
|
82
|
+
| `FAC-SEM-011` | `ready` carries no degraded source |
|
|
83
|
+
| `FAC-SEM-012` | commands start with an absolute or `~/` executable |
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/surfaces-and-auth.md
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Surfaces and auth
|
|
2
|
+
|
|
3
|
+
## MCP for agents
|
|
4
|
+
|
|
5
|
+
- One server inside the service process, `streamable-http`, at `/mcp` on the service
|
|
6
|
+
origin, stateless JSON responses unless the tools stream.
|
|
7
|
+
- Authenticate with the service token in the declared header. OAuth is for remote
|
|
8
|
+
multi-user servers, not for a loopback service.
|
|
9
|
+
- Register it into a client config with the service's own command
|
|
10
|
+
(`<tool> mcp-register`), which writes `{"type":"http","url":"http://127.0.0.1:<port>/mcp","headers":{...}}`
|
|
11
|
+
atomically, aborts if the file changed meanwhile, and keeps one 0600 backup. The token
|
|
12
|
+
goes in `headers`, never in the URL — every log that records a URL prints its query
|
|
13
|
+
string.
|
|
14
|
+
- Ship server `instructions` that tell a caller where to start and that every answer
|
|
15
|
+
carries `degraded`.
|
|
16
|
+
|
|
17
|
+
## CLI for people and scripts
|
|
18
|
+
|
|
19
|
+
`<tool> service status|start|stop|restart` (launchctl on the label, then the
|
|
20
|
+
well-known document), `<tool> dashboard` (asks for a login code and opens it; with
|
|
21
|
+
Fabric Dashboards installed it opens the service there instead), `<tool> doctor --json`
|
|
22
|
+
(the same checks the probe runs, plus provider keys), `--json` on every command. Print
|
|
23
|
+
a sign-in link only when stdout is a terminal.
|
|
24
|
+
|
|
25
|
+
## A2A for remote agents
|
|
26
|
+
|
|
27
|
+
Remote reach goes through A2A `1.0` over HTTPS or an authenticated gateway in front of
|
|
28
|
+
MCP — never by binding a LAN address. A tunnel (Tailscale Serve and the like) is an
|
|
29
|
+
outward surface with its own token lifetime; watch it like a service, because nothing
|
|
30
|
+
else will notice it expire.
|
|
31
|
+
|
|
32
|
+
## Token
|
|
33
|
+
|
|
34
|
+
- Created once with `ensure_token` at first start, after the instance lock.
|
|
35
|
+
- Stored at the descriptor's `auth.tokenFile`, 0600, owner-checked, never a symlink.
|
|
36
|
+
- Compared in constant time (`token_matches`).
|
|
37
|
+
- Rotation is an operator act: write a new file, restart, re-run `mcp-register`.
|
|
38
|
+
- Same-user processes can read it. The protocol defends against web pages and
|
|
39
|
+
mistakes, not against hostile code running as the operator — say so in SECURITY.md.
|
|
40
|
+
|
|
41
|
+
## Dashboard session
|
|
42
|
+
|
|
43
|
+
- `POST /fabric/v1/login-code` (token) → single-use code, at most 120 s, persisted as
|
|
44
|
+
used before it is honoured, burned even when expired.
|
|
45
|
+
- `GET /fabric/v1/login?code=` → `HttpOnly; SameSite=Strict` cookie, 302 to the
|
|
46
|
+
dashboard. Sessions are HMAC-signed; `revoke_all` rotates the key and ends every
|
|
47
|
+
session at once.
|
|
48
|
+
- Browser writes need the cookie **and** a custom header such as `X-<Tool>-Request: 1`,
|
|
49
|
+
which forces a preflight the service never answers.
|
|
50
|
+
- A dashboard that is deliberately read-open to local pages declares
|
|
51
|
+
`"login": false`; its writes still need the header.
|
|
52
|
+
|
|
53
|
+
## Headers every response carries
|
|
54
|
+
|
|
55
|
+
`Cache-Control: no-store`, `X-Content-Type-Options: nosniff`, a CSP with no inline
|
|
56
|
+
script, and `frame-ancestors 'self'` unless the page must never be embedded. Fabric
|
|
57
|
+
Dashboards shows pages in a native view, not a frame, so `X-Frame-Options: DENY` does
|
|
58
|
+
not stop it.
|