@passioncode-ai/fabric-agent-adapter 0.13.0 → 0.15.0
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 +62 -0
- package/README.md +3 -3
- package/package.json +1 -1
- package/plugins/fabric-agent-adapter/.claude-plugin/plugin.json +1 -1
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/SKILL.md +3 -3
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/adapt_project.py +2 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +2 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/demo.html +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-button.js +280 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-dashboard.css +30 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-dashboard.js +2 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/kit.json +9 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard-kit.md +31 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/protocol.md +5 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/dashboard_kit.py +7 -4
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-service.mjs +1 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_service.py +1 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/sample_service.py +4 -2
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,67 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.15.0 - 2026-10-12
|
|
4
|
+
|
|
5
|
+
Fabric's button in the dashboard kit (contract DEC-0040, FB-7). With it, FAA-20 is complete. The contract pin is
|
|
6
|
+
unchanged (`0cacd08`).
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
- **`assets/dashboard-kit/fabric-button.js`** (kit `fabric-dashboard-kit` 1.1.0) is one floating menu button that
|
|
11
|
+
opens Fabric on the service. It is driven by `data-service`, `data-service-name`, `data-project`,
|
|
12
|
+
`data-project-name` and `data-runner`, and implements FB-1 to FB-6:
|
|
13
|
+
- **Menu (FB-4):** "Chat with Fabric about …", plus "Open a coding session in …" when the page knows its project.
|
|
14
|
+
Arrow keys, Home, End and Escape work.
|
|
15
|
+
- **Placement (FB-2):** bottom-right, or bottom-left right-to-left, 16 px plus the safe area. The page keeps the
|
|
16
|
+
button's height plus 16 px free at the end of its scroll.
|
|
17
|
+
- **Look (FB-3):** the mark alone below 480 px. The accessible name is always "Open Fabric". Colours are `--ink`
|
|
18
|
+
on `--panel` with an `--edge` boundary, the pairs the themes are already measured on.
|
|
19
|
+
- **Host button (FB-6):** the page hides its own button under the host's `fabric_button` cookie `owner=host`.
|
|
20
|
+
- **Fabric absent (FB-5):** "Fabric is not installed — get it at passioncode.ai/fabric" shows when the cookie or
|
|
21
|
+
the server says Fabric is absent, or when the page is still visible and focused 1.5 s after a link was followed.
|
|
22
|
+
|
|
23
|
+
Links are built with a port of the contract's `buildFabricLink` and `parseFabricLink`, with the FL-4 credential
|
|
24
|
+
detector. A value the grammar refuses means no button, never a bad link. A press only opens a view (FB-8). The
|
|
25
|
+
script is CSP-clean, and the only address it names is passioncode.ai/fabric.
|
|
26
|
+
- `test/fixtures/contract/fabric-links.json` holds 42 parse and 9 build cases. Every expected answer comes from the
|
|
27
|
+
contract's own functions at the pin, through `make_link_vectors.mjs`.
|
|
28
|
+
- `test/fabric-button.test.mjs` covers:
|
|
29
|
+
- the grammar against those vectors;
|
|
30
|
+
- the cookie and the menu items;
|
|
31
|
+
- the CSP check;
|
|
32
|
+
- the behaviour in WebKit through Playwright: placement, size, scroll clearance, keyboard, right-to-left, the
|
|
33
|
+
host cookie, Fabric absent, and the 1.5 s test. These are skipped when Playwright is absent.
|
|
34
|
+
|
|
35
|
+
Eight planted defects were each caught.
|
|
36
|
+
- The sample service's dashboard shows the button. The layout probe passes all 17 rules on it in WebKit.
|
|
37
|
+
- `references/dashboard-kit.md` has a "Fabric's button" section. The scenario eval `fabric-button-on-a-dashboard`
|
|
38
|
+
came first.
|
|
39
|
+
|
|
40
|
+
### Changed
|
|
41
|
+
|
|
42
|
+
- The dashboard kit is 1.1.0. `dashboard_kit.py install` also copies `fabric-button.js`, and `build` digests every
|
|
43
|
+
copied file. `DASHBOARD_KIT` in both service kits names `fabric-dashboard-kit@1.1.0`.
|
|
44
|
+
|
|
45
|
+
## 0.14.0 - 2026-10-12
|
|
46
|
+
|
|
47
|
+
The contract pin moves to Fabric Agent Contract `main` `0cacd08`, which includes PR #46 (DEC-0046, runner and model
|
|
48
|
+
profiles) and PR #47 (CT-31). Bundles issued under the DEC-0045 revision and every earlier supported one stay valid
|
|
49
|
+
(FAA-30).
|
|
50
|
+
|
|
51
|
+
### Changed
|
|
52
|
+
|
|
53
|
+
- **Default contract pin `2eb91c0` → `0cacd08b0e95b6abf131eae1206b60792138bc3c`.** `2eb91c0` joins
|
|
54
|
+
`SUPPORTED_CONTRACT_COMMITS`. The revisions between add DEC-0046's model and runner profile schemas, a model
|
|
55
|
+
catalogue, an optional `configuration` on the well-known document and an optional `route` on usage receipts.
|
|
56
|
+
They also tighten `modelClasses` in a grant to the catalogue's closed vocabulary, which the kits do not write. Later
|
|
57
|
+
merges change fixtures and the contract's privacy check only. The provider manifest and the MCP, A2A and
|
|
58
|
+
local-runner profiles are unchanged. The compiled-schema arms at `2ce3922` and `0cacd08` agree.
|
|
59
|
+
- **The shared delegation vectors are regenerated with the contract's verifier at the new pin.** The one case the
|
|
60
|
+
contract used to throw on, a signed token with no `act`, now carries the contract's own answer (`unauthenticated` /
|
|
61
|
+
`malformed-chain`), which is the answer the kits already gave. The other 81 delegation answers are unchanged. The
|
|
62
|
+
tests no longer have a `contractThrew` path; they require that the contract answers every case.
|
|
63
|
+
- `references/protocol.md` names DEC-0046's `configuration` and `route`. The kits do not emit them yet (FAA-19).
|
|
64
|
+
|
|
3
65
|
## 0.13.0 - 2026-10-12
|
|
4
66
|
|
|
5
67
|
The scaffolder writes what a capability does outside itself, and what a run may cost (FAA-25; target architecture v4,
|
package/README.md
CHANGED
|
@@ -203,7 +203,7 @@ Scaffolding is non-destructive by default. It writes only the paths declared in
|
|
|
203
203
|
The default pin for new bundles is [`fabric-contract.lock.json`](fabric-contract.lock.json):
|
|
204
204
|
|
|
205
205
|
- version: `0.1.0`
|
|
206
|
-
- commit: `
|
|
206
|
+
- commit: `0cacd08b0e95b6abf131eae1206b60792138bc3c`
|
|
207
207
|
|
|
208
208
|
Every other mention of the default contract revision — this section, the skills' metadata, the
|
|
209
209
|
skill card, `adapt_project.py` — must equal it; `python3 test/validate.py` fails a tree
|
|
@@ -230,8 +230,8 @@ FABRIC_CONTRACT_OLD=/path/to/legacy-contract FABRIC_CONTRACT_NEW=/path/to/curren
|
|
|
230
230
|
```
|
|
231
231
|
|
|
232
232
|
Without both environment variables, those tests explicitly skip as `NOT_RUN`.
|
|
233
|
-
This source checkout is 0.
|
|
234
|
-
were never released); 0.
|
|
233
|
+
This source checkout is 0.15.0. 0.14.0 is the latest published GitHub/npm release (2026-10-12; 0.7.0 and 0.11.0
|
|
234
|
+
were never released); 0.15.0 is published by its `v0.15.0` tag. Source delivery does not update installed skills or live sessions.
|
|
235
235
|
|
|
236
236
|
## License
|
|
237
237
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@passioncode-ai/fabric-agent-adapter",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"description": "Create Fabric-compatible agents, run them as always-alive local services with dashboards (fabric-service/0.1), and adapt existing projects to the Fabric Agent Contract. This package is the installer CLI.",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "python3 test/validate.py && python3 -m unittest discover -s test -q && node --test 'test/*.test.mjs'"
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"name": "fabric-agent-adapter",
|
|
4
4
|
"displayName": "Fabric Agent Adapter",
|
|
5
5
|
"description": "Create new Fabric-compatible agents, build them as always-alive local services with dashboards (fabric-service/0.1), and adapt existing projects to the Fabric Agent Contract through evidence-backed MCP, A2A, or local-runner provider bundles.",
|
|
6
|
-
"version": "0.
|
|
6
|
+
"version": "0.15.0",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "PassionCode.ai",
|
|
9
9
|
"url": "https://passioncode.ai/"
|
|
@@ -5,9 +5,9 @@ license: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
|
|
|
5
5
|
compatibility: Requires filesystem access and Python 3.9+. Exact schema checks additionally need git, Node.js, pnpm, and the pinned fabric-agent-contract checkout (a public repository). Works without those tools in an explicitly degraded structural-check mode.
|
|
6
6
|
metadata:
|
|
7
7
|
author: PassionCode.ai
|
|
8
|
-
version: "0.
|
|
8
|
+
version: "0.15.0"
|
|
9
9
|
contract-version: "0.1.0"
|
|
10
|
-
contract-commit: "
|
|
10
|
+
contract-commit: "0cacd08b0e95b6abf131eae1206b60792138bc3c"
|
|
11
11
|
---
|
|
12
12
|
|
|
13
13
|
# Adapting projects to Fabric
|
|
@@ -70,7 +70,7 @@ Use exactly:
|
|
|
70
70
|
|
|
71
71
|
- contract version `0.1.0`;
|
|
72
72
|
- repository `https://github.com/passioncode-ai/fabric-agent-contract`;
|
|
73
|
-
- commit `
|
|
73
|
+
- commit `0cacd08b0e95b6abf131eae1206b60792138bc3c`.
|
|
74
74
|
|
|
75
75
|
This is the default for new bundles. When checking an issued legacy bundle, preserve its
|
|
76
76
|
explicit supported lock and use that exact clean checkout. Never silently upgrade or
|
package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/adapt_project.py
CHANGED
|
@@ -18,10 +18,10 @@ from urllib.parse import urlparse
|
|
|
18
18
|
|
|
19
19
|
CONTRACT_VERSION = "0.1.0"
|
|
20
20
|
CONTRACT_REPOSITORY = "https://github.com/passioncode-ai/fabric-agent-contract"
|
|
21
|
-
CONTRACT_COMMIT = "
|
|
21
|
+
CONTRACT_COMMIT = "0cacd08b0e95b6abf131eae1206b60792138bc3c"
|
|
22
22
|
# #region supported-contract-revisions — docs: README.md#contract-pin
|
|
23
23
|
# Only this declaration may name a legacy revision in live files.
|
|
24
|
-
SUPPORTED_CONTRACT_COMMITS = (CONTRACT_COMMIT, "8e49fbd5fbbf230e32c1969cd24ac3c22422f0a5", "a83bec6b47776af688402c18efec507143061f68", "52da526d7cc1e063d74c5b520dbc03d0ace15246", "78b2da018d2c4bd9ac1bde1dc3b7f1a110a175f5", "623bf61358c339cb10297807b3f024b5d9f1f327", "23f9fda4c05f8a3852246ee98d4f2adf74ed0875", "9091d3d6606b0c4b5591a38c3674356a01fd7f55", "df55c8c54a23251342a7ee57ba95642b7eb39e61", "2ce392291c6668598d12cd38327e24696b5ca15c")
|
|
24
|
+
SUPPORTED_CONTRACT_COMMITS = (CONTRACT_COMMIT, "2eb91c00e852d9c31d804310d38fad91589fcbac", "8e49fbd5fbbf230e32c1969cd24ac3c22422f0a5", "a83bec6b47776af688402c18efec507143061f68", "52da526d7cc1e063d74c5b520dbc03d0ace15246", "78b2da018d2c4bd9ac1bde1dc3b7f1a110a175f5", "623bf61358c339cb10297807b3f024b5d9f1f327", "23f9fda4c05f8a3852246ee98d4f2adf74ed0875", "9091d3d6606b0c4b5591a38c3674356a01fd7f55", "df55c8c54a23251342a7ee57ba95642b7eb39e61", "2ce392291c6668598d12cd38327e24696b5ca15c")
|
|
25
25
|
# #endregion supported-contract-revisions
|
|
26
26
|
INTEROP_KEY = "https://fabric.passioncode.ai/agent-contract/extensions/interop/0.1"
|
|
27
27
|
MCP_REVISION = "2026-07-28"
|
|
@@ -16,10 +16,10 @@ license: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
|
|
|
16
16
|
compatibility: Python 3.9+ or Node.js 20+ for the kits; the probe needs Python 3.9+. launchd steps are macOS-only (Linux services use lifecycle manager none until a systemd adapter exists). Dashboard handoff optionally uses Fabric Dashboards MCP link/host_status/open; without it, report unresolved host capability. The contract checkout is optional.
|
|
17
17
|
metadata:
|
|
18
18
|
author: PassionCode.ai
|
|
19
|
-
version: "0.
|
|
19
|
+
version: "0.15.0"
|
|
20
20
|
contract-version: "0.1.0"
|
|
21
21
|
extension: "fabric-service/0.1"
|
|
22
|
-
extension-commit: "
|
|
22
|
+
extension-commit: "0cacd08b0e95b6abf131eae1206b60792138bc3c"
|
|
23
23
|
---
|
|
24
24
|
|
|
25
25
|
# Building Fabric services
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/demo.html
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
<title>Example agent · dashboard kit demo</title>
|
|
8
8
|
<link rel="stylesheet" href="fabric-dashboard.css">
|
|
9
9
|
<script src="fabric-dashboard.js" defer></script>
|
|
10
|
+
<script src="fabric-button.js" defer data-service="example-agent.default" data-service-name="Example Agent" data-project="project-a" data-project-name="Project A"></script>
|
|
10
11
|
</head>
|
|
11
12
|
<body>
|
|
12
13
|
<a class="fd-skip" href="#overview">Skip to content</a>
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
/* fabric-dashboard-kit@1.1.0 · Fabric's button (Fabric Agent Contract DEC-0040, FB-1…FB-8, FL-1…FL-6)
|
|
2
|
+
Load it same-origin, after fabric-dashboard.css, with the service it opens Fabric on:
|
|
3
|
+
<script src="fabric-button.js" defer data-service="example-agent.default" data-service-name="Example Agent"
|
|
4
|
+
data-project="project-a" data-project-name="Project A"></script>
|
|
5
|
+
One floating menu button: "Chat with Fabric about <service>" and, when the page knows its project, "Open a coding
|
|
6
|
+
session in <project>". Its links carry ids only and open a view in Fabric, nothing more (FB-8). It hides itself
|
|
7
|
+
when the host draws its own (the fabric_button cookie, FB-6) and says how to get Fabric when Fabric is absent
|
|
8
|
+
(FB-5). No inline script, no eval, nothing from a CDN; the only address it names is passioncode.ai/fabric.
|
|
9
|
+
Do not edit in place; `dashboard_kit.py install` replaces it on an upgrade. */
|
|
10
|
+
(function (root) {
|
|
11
|
+
'use strict';
|
|
12
|
+
|
|
13
|
+
var VERSION = '1.1.0';
|
|
14
|
+
var COOKIE = 'fabric_button';
|
|
15
|
+
var GET_FABRIC = 'https://passioncode.ai/fabric';
|
|
16
|
+
var ABSENT_AFTER_MS = 1500;
|
|
17
|
+
var MAX_LINK_LENGTH = 2048;
|
|
18
|
+
|
|
19
|
+
// ---- fabric:// links: a port of the contract's src/fabric-links.ts and src/secret-shape.ts (FL-1…FL-6) ----
|
|
20
|
+
|
|
21
|
+
var AGENT_ID = /^[a-z][a-z0-9-]{1,62}(\.[a-z][a-z0-9-]{0,31})?$/;
|
|
22
|
+
var PROJECT_REF = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
|
|
23
|
+
var RUNNER_KIND = /^[a-z][a-z0-9-]{1,62}$/;
|
|
24
|
+
var SHAPE = /^fabric:\/\/(chat|session\/new)\/?(?:\?([^#]*))?(?:#(.*))?$/;
|
|
25
|
+
var KNOWN = {
|
|
26
|
+
chat: { service: AGENT_ID, project: PROJECT_REF },
|
|
27
|
+
'session.new': { project: PROJECT_REF, runner: RUNNER_KIND }
|
|
28
|
+
};
|
|
29
|
+
var PROMPT_WORDS = ['prompt', 'message', 'msg', 'text', 'instruction', 'instructions'];
|
|
30
|
+
var CREDENTIAL_WORDS = ['token', 'secret', 'password', 'passwd', 'pass', 'passphrase', 'credential', 'credentials', 'cookie',
|
|
31
|
+
'authorization', 'bearer', 'jwt', 'otp', 'apikey', 'signature', 'sig', 'accesstoken', 'idtoken', 'authtoken',
|
|
32
|
+
'refreshtoken', 'sessionid', 'sessiontoken', 'logincode', 'authcode', 'privatekey', 'clientsecret', 'secretkey', 'accesskey'];
|
|
33
|
+
var CREDENTIAL_PAIRS = {
|
|
34
|
+
key: ['private', 'api', 'access', 'secret', 'session', 'auth', 'signing', 'encryption', 'master', 'client', 'login', 'refresh'],
|
|
35
|
+
auth: ['token', 'code', 'header'],
|
|
36
|
+
session: ['token', 'id'],
|
|
37
|
+
code: ['login', 'auth', 'access', 'session']
|
|
38
|
+
};
|
|
39
|
+
var JWT_SHAPE = /eyJ[A-Za-z0-9_-]{2,}\.[A-Za-z0-9_-]{2,}\.[A-Za-z0-9_-]*/;
|
|
40
|
+
var SECRET_VALUE = /(^bearer\s.+|sk-[A-Za-z0-9_-]{16,}|gh[pousr]_[A-Za-z0-9]{20,}|xox[abposr]-[A-Za-z0-9-]{10,}|AKIA[0-9A-Z]{16}|glpat-[A-Za-z0-9_-]{20,})/i;
|
|
41
|
+
var LONG_RUN = /[A-Za-z0-9+/=_-]{40,}/g;
|
|
42
|
+
|
|
43
|
+
function nameWords(name) {
|
|
44
|
+
return name.replace(/([a-z0-9])([A-Z])/g, '$1 $2').replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2')
|
|
45
|
+
.split(/[\s._-]+/).filter(Boolean).map(function (w) { return w.toLowerCase(); });
|
|
46
|
+
}
|
|
47
|
+
function withSingulars(words) {
|
|
48
|
+
return words.concat(words.filter(function (w) { return w.length > 2 && /s$/.test(w); }).map(function (w) { return w.slice(0, -1); }));
|
|
49
|
+
}
|
|
50
|
+
function credentialName(name) {
|
|
51
|
+
var split = nameWords(name);
|
|
52
|
+
var words = withSingulars(split);
|
|
53
|
+
if (words.some(function (w) { return CREDENTIAL_WORDS.indexOf(w) >= 0; })) return true;
|
|
54
|
+
if (split.length === 1 && words.some(function (w) { return Object.prototype.hasOwnProperty.call(CREDENTIAL_PAIRS, w); })) return true;
|
|
55
|
+
return Object.keys(CREDENTIAL_PAIRS).some(function (w) {
|
|
56
|
+
return words.indexOf(w) >= 0 && CREDENTIAL_PAIRS[w].some(function (p) { return words.indexOf(p) >= 0; });
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
function longSecret(value) {
|
|
60
|
+
return (value.match(LONG_RUN) || []).some(function (run) { return /[a-z]/.test(run) && /[A-Z]/.test(run) && /[0-9]/.test(run); });
|
|
61
|
+
}
|
|
62
|
+
function secretValue(value) { return JWT_SHAPE.test(value) || SECRET_VALUE.test(value) || longSecret(value); }
|
|
63
|
+
function decodeKeepingPlus(raw) { try { return decodeURIComponent(raw); } catch (e) { return raw; } }
|
|
64
|
+
function firstKey(text) { var it = new URLSearchParams(text).keys().next(); return it.done ? '' : it.value; }
|
|
65
|
+
function rawValues(query) {
|
|
66
|
+
var values = new Map();
|
|
67
|
+
query.split('&').forEach(function (piece) {
|
|
68
|
+
if (piece === '') return;
|
|
69
|
+
var at = piece.indexOf('=');
|
|
70
|
+
var name = firstKey(at < 0 ? piece : piece.slice(0, at));
|
|
71
|
+
values.set(name, (values.get(name) || []).concat(at < 0 ? '' : decodeKeepingPlus(piece.slice(at + 1))));
|
|
72
|
+
});
|
|
73
|
+
return values;
|
|
74
|
+
}
|
|
75
|
+
function promptName(name) { return nameWords(name).some(function (w) { return PROMPT_WORDS.indexOf(w) >= 0; }); }
|
|
76
|
+
function flat(lists) { return lists.reduce(function (a, b) { return a.concat(b); }, []); }
|
|
77
|
+
|
|
78
|
+
/** The contract's parseFabricLink: {ok: true, link, ignored} or {ok: false, problems}. */
|
|
79
|
+
function parseFabricLink(url) {
|
|
80
|
+
if (url.length > MAX_LINK_LENGTH) return { ok: false, problems: ['a link is at most ' + MAX_LINK_LENGTH + ' characters'] };
|
|
81
|
+
var match = SHAPE.exec(url.replace(/^fabric:/i, 'fabric:'));
|
|
82
|
+
if (!match) return { ok: false, problems: ['not fabric://chat or fabric://session/new'] };
|
|
83
|
+
var action = match[1] === 'chat' ? 'chat' : 'session.new';
|
|
84
|
+
var params = new URLSearchParams(match[2] || '');
|
|
85
|
+
var problems = [];
|
|
86
|
+
var fragment = match[3] || '';
|
|
87
|
+
if (fragment) {
|
|
88
|
+
var pieces = Array.from(new URLSearchParams(fragment));
|
|
89
|
+
var raw = flat(Array.from(rawValues(fragment).values()));
|
|
90
|
+
if (secretValue(fragment) || raw.some(secretValue) || pieces.some(function (p) { return (p[1] !== '' && credentialName(p[0])) || secretValue(p[1]) || secretValue(p[0]); })) {
|
|
91
|
+
problems.push('the fragment could carry a credential; a link never does');
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
var ignored = [];
|
|
95
|
+
var values = {};
|
|
96
|
+
var known = KNOWN[action];
|
|
97
|
+
var rawQuery = rawValues(match[2] || '');
|
|
98
|
+
Array.from(new Set(Array.from(params.keys()))).forEach(function (name) {
|
|
99
|
+
var all = params.getAll(name);
|
|
100
|
+
if (promptName(name)) { problems.push('the parameter ' + name + ' carries words to act on; a link carries ids only'); return; }
|
|
101
|
+
if (credentialName(name) || secretValue(name) || all.some(secretValue) || (rawQuery.get(name) || []).some(secretValue)) {
|
|
102
|
+
problems.push('the parameter ' + name + ' could carry a credential; a link never does'); return;
|
|
103
|
+
}
|
|
104
|
+
var grammar = Object.prototype.hasOwnProperty.call(known, name) ? known[name] : undefined;
|
|
105
|
+
if (!grammar) { ignored.push(name); return; }
|
|
106
|
+
if (all.length > 1) { problems.push(name + ' is given ' + all.length + ' times'); return; }
|
|
107
|
+
var value = all[0] || '';
|
|
108
|
+
if (!grammar.test(value)) {
|
|
109
|
+
problems.push(name + ' is not a valid ' + (name === 'service' ? 'agent id' : name === 'runner' ? 'runner kind' : 'project id or slug')); return;
|
|
110
|
+
}
|
|
111
|
+
values[name] = value;
|
|
112
|
+
});
|
|
113
|
+
var named = function (re) { return problems.some(function (p) { return re.test(p); }); };
|
|
114
|
+
if (action === 'chat' && values.service === undefined && values.project === undefined && !named(/^(service|project) /)) problems.push('a chat link names a service, a project or both');
|
|
115
|
+
if (action === 'session.new' && values.project === undefined && !named(/^project /)) problems.push('a session link names its project');
|
|
116
|
+
if (problems.length) return { ok: false, problems: problems };
|
|
117
|
+
var link = action === 'chat' ? { action: action } : { action: action, project: values.project };
|
|
118
|
+
if (action === 'chat') { if (values.service) link.service = values.service; if (values.project) link.project = values.project; }
|
|
119
|
+
else if (values.runner) link.runner = values.runner;
|
|
120
|
+
return { ok: true, link: link, ignored: ignored };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** The contract's buildFabricLink: throws on a value the grammar refuses, so a page cannot emit a bad link. */
|
|
124
|
+
function buildFabricLink(link) {
|
|
125
|
+
var query = new URLSearchParams();
|
|
126
|
+
if (link.action === 'chat') {
|
|
127
|
+
if (link.service !== undefined) query.set('service', link.service);
|
|
128
|
+
if (link.project !== undefined) query.set('project', link.project);
|
|
129
|
+
} else {
|
|
130
|
+
query.set('project', link.project);
|
|
131
|
+
if (link.runner !== undefined) query.set('runner', link.runner);
|
|
132
|
+
}
|
|
133
|
+
var url = 'fabric://' + (link.action === 'chat' ? 'chat' : 'session/new') + '?' + query.toString();
|
|
134
|
+
var parsed = parseFabricLink(url);
|
|
135
|
+
if (!parsed.ok) throw new TypeError(parsed.problems.join('; '));
|
|
136
|
+
return url;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** FB-6: the host's cookie as {owner, fabric}, or null when absent or of an unknown version. */
|
|
140
|
+
function parseButtonCookie(cookieHeader) {
|
|
141
|
+
if (typeof cookieHeader !== 'string' || !cookieHeader) return null;
|
|
142
|
+
var parts = cookieHeader.split(';');
|
|
143
|
+
for (var i = 0; i < parts.length; i++) {
|
|
144
|
+
var part = parts[i].replace(/^\s+/, '');
|
|
145
|
+
if (part.indexOf(COOKIE + '=') !== 0) continue;
|
|
146
|
+
var raw = part.slice(COOKIE.length + 1);
|
|
147
|
+
if (raw.charAt(0) === '"' && raw.charAt(raw.length - 1) === '"') raw = raw.slice(1, -1);
|
|
148
|
+
var fields = new URLSearchParams(decodeKeepingPlus(raw));
|
|
149
|
+
if (fields.get('v') !== '1') return null;
|
|
150
|
+
var out = {};
|
|
151
|
+
if (fields.get('owner') === 'host' || fields.get('owner') === 'page') out.owner = fields.get('owner');
|
|
152
|
+
if (fields.get('fabric') === 'present' || fields.get('fabric') === 'absent') out.fabric = fields.get('fabric');
|
|
153
|
+
return out;
|
|
154
|
+
}
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** The menu items for a page: the chat link always, the session link when the page knows its project. */
|
|
159
|
+
function menuItems(options) {
|
|
160
|
+
var items = [];
|
|
161
|
+
var chat = { action: 'chat', service: options.service };
|
|
162
|
+
if (options.project) chat.project = options.project;
|
|
163
|
+
items.push({ label: 'Chat with Fabric about ' + (options.serviceName || options.service), href: buildFabricLink(chat) });
|
|
164
|
+
if (options.project) {
|
|
165
|
+
var session = { action: 'session.new', project: options.project };
|
|
166
|
+
if (options.runner) session.runner = options.runner;
|
|
167
|
+
items.push({ label: 'Open a coding session in ' + (options.projectName || options.project), href: buildFabricLink(session) });
|
|
168
|
+
}
|
|
169
|
+
return items;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ---- the button (FB-1…FB-6) ----
|
|
173
|
+
|
|
174
|
+
var SVG = 'http://www.w3.org/2000/svg';
|
|
175
|
+
|
|
176
|
+
function mark(doc) {
|
|
177
|
+
var svg = doc.createElementNS(SVG, 'svg');
|
|
178
|
+
svg.setAttribute('viewBox', '0 0 24 24');
|
|
179
|
+
svg.setAttribute('aria-hidden', 'true');
|
|
180
|
+
svg.setAttribute('focusable', 'false');
|
|
181
|
+
svg.setAttribute('class', 'fd-fabric-button-mark');
|
|
182
|
+
var path = doc.createElementNS(SVG, 'path');
|
|
183
|
+
path.setAttribute('d', 'M5 5h14v3H8v3h9v3H8v5H5z'); // the Fabric mark: an F
|
|
184
|
+
path.setAttribute('fill', 'currentColor');
|
|
185
|
+
svg.appendChild(path);
|
|
186
|
+
return svg;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function el(doc, tag, attrs, text) {
|
|
190
|
+
var node = doc.createElement(tag);
|
|
191
|
+
Object.keys(attrs || {}).forEach(function (k) { node.setAttribute(k, attrs[k]); });
|
|
192
|
+
if (text !== undefined) node.textContent = text;
|
|
193
|
+
return node;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Mount the button in `doc`. Returns the button, or null when the host draws its own (FB-1, FB-6). */
|
|
197
|
+
function mount(doc, options) {
|
|
198
|
+
var cookie = parseButtonCookie(doc.cookie);
|
|
199
|
+
if (cookie && cookie.owner === 'host') return null;
|
|
200
|
+
var items = menuItems(options);
|
|
201
|
+
var absent = options.fabric === 'absent' || (cookie && cookie.fabric === 'absent');
|
|
202
|
+
|
|
203
|
+
var wrap = el(doc, 'div', { 'class': 'fd-fabric-button-wrap' });
|
|
204
|
+
var button = el(doc, 'button', { type: 'button', 'class': 'fd-fabric-button', 'aria-haspopup': 'menu', 'aria-expanded': 'false', 'aria-label': 'Open Fabric' });
|
|
205
|
+
button.appendChild(mark(doc));
|
|
206
|
+
button.appendChild(el(doc, 'span', { 'class': 'fd-fabric-button-label', 'aria-hidden': 'true' }, 'Fabric'));
|
|
207
|
+
var menu = el(doc, 'div', { role: 'menu', 'class': 'fd-fabric-button-menu', 'aria-label': 'Fabric' });
|
|
208
|
+
menu.hidden = true;
|
|
209
|
+
var links = items.map(function (item) {
|
|
210
|
+
var a = el(doc, 'a', { role: 'menuitem', href: item.href, tabindex: '-1', 'class': 'fd-fabric-button-item' }, item.label);
|
|
211
|
+
menu.appendChild(a);
|
|
212
|
+
return a;
|
|
213
|
+
});
|
|
214
|
+
var note = el(doc, 'p', { 'class': 'fd-fabric-button-note', role: 'note' });
|
|
215
|
+
note.appendChild(doc.createTextNode('Fabric is not installed — get it at '));
|
|
216
|
+
note.appendChild(el(doc, 'a', { href: GET_FABRIC, target: '_blank', rel: 'noopener', 'class': 'fd-fabric-button-get' }, 'passioncode.ai/fabric'));
|
|
217
|
+
note.hidden = !absent;
|
|
218
|
+
menu.appendChild(note);
|
|
219
|
+
wrap.appendChild(menu);
|
|
220
|
+
wrap.appendChild(button);
|
|
221
|
+
doc.body.appendChild(wrap);
|
|
222
|
+
doc.documentElement.classList.add('fd-has-fabric-button');
|
|
223
|
+
|
|
224
|
+
function open(focusFirst) {
|
|
225
|
+
menu.hidden = false;
|
|
226
|
+
button.setAttribute('aria-expanded', 'true');
|
|
227
|
+
if (focusFirst && links.length) links[0].focus();
|
|
228
|
+
}
|
|
229
|
+
function close(refocus) {
|
|
230
|
+
menu.hidden = true;
|
|
231
|
+
button.setAttribute('aria-expanded', 'false');
|
|
232
|
+
if (refocus) button.focus();
|
|
233
|
+
}
|
|
234
|
+
function move(by) {
|
|
235
|
+
var at = links.indexOf(doc.activeElement);
|
|
236
|
+
links[(at + by + links.length) % links.length].focus();
|
|
237
|
+
}
|
|
238
|
+
button.addEventListener('click', function () { if (menu.hidden) open(true); else close(true); });
|
|
239
|
+
button.addEventListener('keydown', function (e) {
|
|
240
|
+
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') { e.preventDefault(); open(false); (e.key === 'ArrowDown' ? links[0] : links[links.length - 1]).focus(); }
|
|
241
|
+
});
|
|
242
|
+
menu.addEventListener('keydown', function (e) {
|
|
243
|
+
if (e.key === 'Escape') { e.preventDefault(); close(true); }
|
|
244
|
+
else if (e.key === 'ArrowDown') { e.preventDefault(); move(1); }
|
|
245
|
+
else if (e.key === 'ArrowUp') { e.preventDefault(); move(-1); }
|
|
246
|
+
else if (e.key === 'Home') { e.preventDefault(); links[0].focus(); }
|
|
247
|
+
else if (e.key === 'End') { e.preventDefault(); links[links.length - 1].focus(); }
|
|
248
|
+
else if (e.key === 'Tab') { close(false); }
|
|
249
|
+
});
|
|
250
|
+
doc.addEventListener('click', function (e) { if (!menu.hidden && !wrap.contains(e.target)) close(false); });
|
|
251
|
+
// FB-5: a page cannot see which schemes this computer handles. If, after following a link, the page is still
|
|
252
|
+
// visible and focused 1.5 s later, Fabric did not take it: say how to get Fabric, once, and never retry.
|
|
253
|
+
links.forEach(function (a) {
|
|
254
|
+
a.addEventListener('click', function () {
|
|
255
|
+
root.setTimeout(function () {
|
|
256
|
+
if (doc.visibilityState === 'visible' && doc.hasFocus()) { note.hidden = false; open(false); }
|
|
257
|
+
}, ABSENT_AFTER_MS);
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
return button;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function optionsFrom(script) {
|
|
264
|
+
var d = script.dataset;
|
|
265
|
+
return { service: d.service, serviceName: d.serviceName, project: d.project, projectName: d.projectName, runner: d.runner, fabric: d.fabric };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
var api = { version: VERSION, parseFabricLink: parseFabricLink, buildFabricLink: buildFabricLink, parseButtonCookie: parseButtonCookie,
|
|
269
|
+
menuItems: menuItems, credentialName: credentialName, secretValue: secretValue, nameWords: nameWords, mount: mount };
|
|
270
|
+
root.FabricButton = api;
|
|
271
|
+
|
|
272
|
+
var doc = root.document;
|
|
273
|
+
var script = doc && doc.currentScript;
|
|
274
|
+
if (script && script.dataset && script.dataset.service) {
|
|
275
|
+
var start = function () {
|
|
276
|
+
try { mount(doc, optionsFrom(script)); } catch (err) { if (root.console) root.console.warn('fabric-button: not shown — ' + err.message); }
|
|
277
|
+
};
|
|
278
|
+
if (doc.readyState === 'loading') doc.addEventListener('DOMContentLoaded', start); else start();
|
|
279
|
+
}
|
|
280
|
+
})(typeof window !== 'undefined' ? window : globalThis);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* fabric-dashboard-kit@1.
|
|
1
|
+
/* fabric-dashboard-kit@1.1.0 · fabric-dashboard/0.1 (Fabric Agent Contract DEC-0036)
|
|
2
2
|
A Fabric service dashboard shown inside a host pane: usable from 360 CSS px up, navigation always
|
|
3
3
|
reachable, wide content in its own scroll box, one vertical scroller, appearance from the host.
|
|
4
4
|
Copy this file next to fabric-dashboard.js; do not edit it in place. Put the page's own rules in
|
|
@@ -196,3 +196,32 @@ pre.fd-scroll, .fd-scroll pre { margin: 0; padding: var(--space-3); font: var(--
|
|
|
196
196
|
|
|
197
197
|
/* ---- appearance owned by the host (ED-6) ---- */
|
|
198
198
|
html[data-fd-host-appearance] [data-fd-appearance-control] { display: none !important; }
|
|
199
|
+
|
|
200
|
+
/* ---- Fabric's button (DEC-0040, FB-1…FB-6; fabric-button.js) ---- */
|
|
201
|
+
/* FB-2: fixed to the bottom-right (bottom-left right-to-left), 16 px plus the safe area; above the content and the
|
|
202
|
+
sticky chrome (z 10), below the page's dialogs. The page keeps the button's height plus 16 px free at the end. */
|
|
203
|
+
:root { --fd-fb-size: 44px; }
|
|
204
|
+
html.fd-has-fabric-button body { padding-bottom: calc(var(--fd-fb-size) + 2 * var(--space-4) + env(safe-area-inset-bottom, 0px)); }
|
|
205
|
+
.fd-fabric-button-wrap { position: fixed; z-index: 15; bottom: calc(var(--space-4) + env(safe-area-inset-bottom, 0px));
|
|
206
|
+
right: calc(var(--space-4) + env(safe-area-inset-right, 0px)); display: flex; flex-direction: column; align-items: flex-end; gap: var(--space-2); }
|
|
207
|
+
[dir="rtl"] .fd-fabric-button-wrap { right: auto; left: calc(var(--space-4) + env(safe-area-inset-left, 0px)); align-items: flex-start; }
|
|
208
|
+
/* FB-3: the mark and the word, 44 × 44 at least; colours from the theme: --ink on --panel (AA), an --edge boundary (3:1). */
|
|
209
|
+
.fd-fabric-button { display: inline-flex; align-items: center; justify-content: center; gap: var(--space-2); min-width: var(--fd-fb-size);
|
|
210
|
+
min-height: var(--fd-fb-size); padding: 0 var(--space-3); font: 600 var(--t-body) var(--font-ui); color: var(--ink); background: var(--panel);
|
|
211
|
+
border: 2px solid var(--edge); border-radius: var(--r-pill); cursor: pointer;
|
|
212
|
+
transition: background-color var(--dur-hover) var(--motion-ease); }
|
|
213
|
+
.fd-fabric-button:hover { background: var(--panel-2); }
|
|
214
|
+
.fd-fabric-button:focus-visible, .fd-fabric-button-item:focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; }
|
|
215
|
+
.fd-fabric-button-mark { width: 20px; height: 20px; flex: none; }
|
|
216
|
+
@media (max-width: 479.98px) { .fd-fabric-button-label { display: none; } .fd-fabric-button { padding: 0; } }
|
|
217
|
+
/* FB-4: the menu; FB-5: the note when Fabric is absent. */
|
|
218
|
+
.fd-fabric-button-menu { min-width: 240px; max-width: min(360px, calc(100vw - 2 * var(--space-4))); padding: var(--space-1); background: var(--panel);
|
|
219
|
+
color: var(--ink); border: 1px solid var(--edge); border-radius: var(--r-card); }
|
|
220
|
+
.fd-fabric-button-menu[hidden] { display: none; }
|
|
221
|
+
.fd-fabric-button-item { display: block; padding: var(--space-2) var(--space-3); min-height: var(--fd-fb-size); box-sizing: border-box;
|
|
222
|
+
color: var(--ink); text-decoration: none; border-radius: var(--r-control); font: var(--t-body)/1.5 var(--font-ui); overflow-wrap: anywhere; }
|
|
223
|
+
.fd-fabric-button-item:hover, .fd-fabric-button-item:focus { background: var(--panel-2); }
|
|
224
|
+
.fd-fabric-button-note { margin: var(--space-1) 0 0; padding: var(--space-2) var(--space-3); border-top: 1px solid var(--border-strong);
|
|
225
|
+
font: var(--t-body)/1.5 var(--font-ui); color: var(--ink); }
|
|
226
|
+
.fd-fabric-button-note[hidden] { display: none; }
|
|
227
|
+
.fd-fabric-button-get { color: var(--ink); text-decoration: underline; }
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* fabric-dashboard-kit@1.
|
|
1
|
+
/* fabric-dashboard-kit@1.1.0 · fabric-dashboard/0.1 (Fabric Agent Contract DEC-0036)
|
|
2
2
|
Load it same-origin with <script src="fabric-dashboard.js" defer></script>: no inline script, no
|
|
3
3
|
eval, nothing from a CDN. It applies the host's appearance (ED-6, ED-8), folds navigation that
|
|
4
4
|
does not fit into a More menu (ED-3), makes wide boxes keyboard-reachable (ED-4) and keeps
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
'use strict';
|
|
9
9
|
|
|
10
10
|
var KIT = 'fabric-dashboard-kit';
|
|
11
|
-
var VERSION = '1.
|
|
11
|
+
var VERSION = '1.1.0';
|
|
12
12
|
/* fabric-dashboard-kit:theme-ids:begin */
|
|
13
13
|
var THEMES = ['graphite', 'fabric', 'paper', 'forest', 'ocean', 'ember', 'contrast'];
|
|
14
14
|
/* fabric-dashboard-kit:theme-ids:end */
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/kit.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fabric-dashboard-kit",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"profile": "fabric-dashboard/0.1",
|
|
5
5
|
"decision": "DEC-0036",
|
|
6
6
|
"declarationKey": "https://fabric.passioncode.ai/agent-contract/extensions/dashboard/0.1",
|
|
@@ -15,14 +15,20 @@
|
|
|
15
15
|
"contrast"
|
|
16
16
|
],
|
|
17
17
|
"files": {
|
|
18
|
-
"fabric-dashboard.css": "
|
|
19
|
-
"fabric-dashboard.js": "
|
|
18
|
+
"fabric-dashboard.css": "498571903fac6dfd2d6645283da2f703fa945f410f2fafa269a8c9c322735c3b",
|
|
19
|
+
"fabric-dashboard.js": "100f55d940d4ed60aa8511cee4c65219c93114eb7d7a66639b0e4e4d7aa05f22",
|
|
20
|
+
"fabric-button.js": "4f1e100936ae65af5539594681e66092ae1834a4952da547a67d0558c9fadbbe",
|
|
20
21
|
"themes.json": "511839f1a1a83eb37fadfd132791474497d5c28c5091700b9aa31ad076e359b5"
|
|
21
22
|
},
|
|
22
23
|
"history": {
|
|
23
24
|
"1.0.0": {
|
|
24
25
|
"fabric-dashboard.css": "3993e6ab5e58d5229bc4dc245aa634fcf88a4f59502e1de90d3d3cfef70528a2",
|
|
25
26
|
"fabric-dashboard.js": "bac615a40c85d8c668588528f9e21dac800aa01fefb56b8ee573c30520ce74a7"
|
|
27
|
+
},
|
|
28
|
+
"1.1.0": {
|
|
29
|
+
"fabric-dashboard.css": "498571903fac6dfd2d6645283da2f703fa945f410f2fafa269a8c9c322735c3b",
|
|
30
|
+
"fabric-dashboard.js": "100f55d940d4ed60aa8511cee4c65219c93114eb7d7a66639b0e4e4d7aa05f22",
|
|
31
|
+
"fabric-button.js": "4f1e100936ae65af5539594681e66092ae1834a4952da547a67d0558c9fadbbe"
|
|
26
32
|
}
|
|
27
33
|
}
|
|
28
34
|
}
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard-kit.md
CHANGED
|
@@ -11,6 +11,7 @@ promises so a service does not re-derive them.
|
|
|
11
11
|
- The rules the page keeps
|
|
12
12
|
- Using the kit
|
|
13
13
|
- Themes and the host's appearance
|
|
14
|
+
- Fabric's button
|
|
14
15
|
- Declaring it
|
|
15
16
|
- Proving it
|
|
16
17
|
- Upgrading an existing dashboard
|
|
@@ -79,6 +80,34 @@ controls while it is present; an unknown `theme` is `graphite`. `data-fd-appeara
|
|
|
79
80
|
what the page applies (declare the same in `appearance`). The page re-reads the cookie when it
|
|
80
81
|
changes (`cookieStore`) and when it becomes visible again.
|
|
81
82
|
|
|
83
|
+
## Fabric's button
|
|
84
|
+
|
|
85
|
+
The kit ships Fabric's button (contract DEC-0040, FB-7): one floating menu button that opens Fabric on this
|
|
86
|
+
service. Load it after the stylesheet, with ids only:
|
|
87
|
+
|
|
88
|
+
```html
|
|
89
|
+
<script src="fabric-button.js" defer data-service="example-agent.default" data-service-name="Example Agent"
|
|
90
|
+
data-project="project-a" data-project-name="Project A"></script>
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
- **The menu (FB-4):** "Chat with Fabric about *Example Agent*" always; "Open a coding session in *Project A*" when
|
|
94
|
+
the page knows its project (`data-runner` picks the runner kind). Arrows move, Escape closes and returns focus.
|
|
95
|
+
- **Placement and look (FB-2, FB-3):** bottom-right (bottom-left right-to-left), 16 px off and clear of the safe
|
|
96
|
+
area, above the sticky chrome and below dialogs; the page keeps its height plus 16 px free at the end of the
|
|
97
|
+
scroll. The mark and the word *Fabric*, the mark alone below 480 px; the accessible name is always *Open Fabric*;
|
|
98
|
+
`--ink` on `--panel` with an `--edge` boundary, the pairs the themes are measured on.
|
|
99
|
+
- **Links (FL-1…FL-6):** built with the kit's port of the contract's `buildFabricLink`; a value outside the grammar
|
|
100
|
+
— or one shaped like a credential — means no button, never a bad link. A press opens a view in Fabric and starts
|
|
101
|
+
nothing (FB-8).
|
|
102
|
+
- **The host (FB-6):** when the host's `fabric_button` cookie says `owner=host`, the page hides its own button.
|
|
103
|
+
- **Fabric absent (FB-5):** when the cookie says `fabric=absent`, or `data-fabric="absent"` (a local service whose
|
|
104
|
+
server checked for Fabric's hub document), or when the page is still visible and focused 1.5 s after a link was
|
|
105
|
+
followed, the menu says "Fabric is not installed — get it at passioncode.ai/fabric". Nothing downloads, nothing
|
|
106
|
+
retries.
|
|
107
|
+
|
|
108
|
+
`window.FabricButton` exposes `buildFabricLink`, `parseFabricLink` and `parseButtonCookie` for a page that needs
|
|
109
|
+
them. The button is optional: a page without the script has no button.
|
|
110
|
+
|
|
82
111
|
## Declaring it
|
|
83
112
|
|
|
84
113
|
When the page meets ED-1…ED-6, say so in the well-known document (ED-9):
|
|
@@ -125,9 +154,9 @@ operator and update only with their go-ahead** — the page is theirs and may be
|
|
|
125
154
|
|
|
126
155
|
## When something is missing
|
|
127
156
|
|
|
128
|
-
- **No Python for the CLI:** copy the
|
|
157
|
+
- **No Python for the CLI:** copy the files from `assets/dashboard-kit/` by hand and compare the
|
|
129
158
|
first line's version with `kit.json`.
|
|
130
|
-
- **The page is built by a framework:** import the
|
|
159
|
+
- **The page is built by a framework:** import the files as static assets; keep them unbundled
|
|
131
160
|
or keep the version marker comment, so `status` can still read the version.
|
|
132
161
|
- **The page needs a fixed width:** not decided by the profile; declare the `minWidth` it needs
|
|
133
162
|
(up to 1280) and say why in the README.
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# fabric-service/0.1 — wire reference
|
|
2
2
|
|
|
3
|
-
Pinned to `fabric-agent-contract` commit `
|
|
3
|
+
Pinned to `fabric-agent-contract` commit `0cacd08b0e95b6abf131eae1206b60792138bc3c`
|
|
4
4
|
(`docs/specification/service.md`, DEC-0015; the usage report DEC-0021; Windows and Linux supervision
|
|
5
5
|
DEC-0032 and DEC-0033; the operator channel DEC-0034 and DEC-0035; the embedded dashboard DEC-0036;
|
|
6
6
|
delegation and the operator session's lifetime DEC-0039; the meter's call window DEC-0041; session handoff DEC-0042;
|
|
7
7
|
canonical forms DEC-0043; `degraded[].setup` and `run.mcp.stdio.scope` DEC-0044; delegation sealed and trust tiers
|
|
8
|
-
DEC-0045). The contract's schemas are normative;
|
|
8
|
+
DEC-0045; runner and model profiles DEC-0046). The contract's schemas are normative;
|
|
9
9
|
this page is the working summary. Extension key:
|
|
10
10
|
`https://fabric.passioncode.ai/agent-contract/extensions/service/0.1`.
|
|
11
11
|
|
|
@@ -61,6 +61,9 @@ No auth; the Host/Origin guard applies; answered from memory in under 100 ms.
|
|
|
61
61
|
- A `degraded[]` entry may carry `setup: true` (DEC-0044): the source waits for a person to finish setting it up
|
|
62
62
|
(a key not entered, a model not chosen). It is not an outage, and a watcher does not alert on it. The kits pass
|
|
63
63
|
entries through unchanged.
|
|
64
|
+
- `configuration` (DEC-0046) MAY say which runner and model profile the service resolved and from where
|
|
65
|
+
(`request`, `agent`, `binding`, `estate`); a usage receipt MAY carry the `route` (`subscription` or `key`). The kits
|
|
66
|
+
do not emit either yet (FAA-19).
|
|
64
67
|
- `build` needs `commit` or `digest` (`sha256:<64 hex>`).
|
|
65
68
|
- `summary`: at most six tiles; `attention: true` counts toward the host's badge.
|
|
66
69
|
- `surfaces.events` is required; `dashboard` and `mcp` when the service has them; `usage`
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/dashboard_kit.py
CHANGED
|
@@ -27,7 +27,7 @@ from typing import Any, Dict, List, Optional, Sequence, Tuple
|
|
|
27
27
|
|
|
28
28
|
KIT_DIR = Path(__file__).resolve().parent.parent / "assets" / "dashboard-kit"
|
|
29
29
|
KIT_NAME = "fabric-dashboard-kit"
|
|
30
|
-
COPIED = ("fabric-dashboard.css", "fabric-dashboard.js")
|
|
30
|
+
COPIED = ("fabric-dashboard.css", "fabric-dashboard.js", "fabric-button.js") # fabric-button.js: DEC-0040, FB-7
|
|
31
31
|
MARKER = re.compile(r"fabric-dashboard-kit@(\d+)\.(\d+)\.(\d+)(?:-[0-9A-Za-z.-]+)?")
|
|
32
32
|
CSS_BEGIN, CSS_END = "/* fabric-dashboard-kit:themes:begin */", "/* fabric-dashboard-kit:themes:end */"
|
|
33
33
|
JS_BEGIN, JS_END = "/* fabric-dashboard-kit:theme-ids:begin */", "/* fabric-dashboard-kit:theme-ids:end */"
|
|
@@ -125,7 +125,10 @@ def build(kit_dir: Path = KIT_DIR, check: bool = False) -> List[str]:
|
|
|
125
125
|
new_js = replace_between(js, JS_BEGIN, JS_END, " var THEMES = %s;" % json.dumps(ids).replace('"', "'"), js_path.name)
|
|
126
126
|
manifest = load_manifest(kit_dir)
|
|
127
127
|
version = manifest["version"]
|
|
128
|
-
|
|
128
|
+
built = {css_path.name: new_css, js_path.name: new_js}
|
|
129
|
+
for name in COPIED:
|
|
130
|
+
built.setdefault(name, (kit_dir / name).read_text(encoding="utf-8"))
|
|
131
|
+
for name, text in built.items():
|
|
129
132
|
if claimed_version_text(text) != version:
|
|
130
133
|
raise KitError("%s says %s, kit.json says %s; raise them together." % (name, claimed_version_text(text), version))
|
|
131
134
|
import hashlib
|
|
@@ -135,7 +138,7 @@ def build(kit_dir: Path = KIT_DIR, check: bool = False) -> List[str]:
|
|
|
135
138
|
changed.append(path.name)
|
|
136
139
|
if not check:
|
|
137
140
|
atomic_write(path, text)
|
|
138
|
-
digest = {name: hashlib.sha256(
|
|
141
|
+
digest = {name: hashlib.sha256(built[name].encode("utf-8")).hexdigest() for name in COPIED}
|
|
139
142
|
digest["themes.json"] = sha256(kit_dir / "themes.json")
|
|
140
143
|
new_manifest = dict(manifest)
|
|
141
144
|
new_manifest["themes"] = ids
|
|
@@ -199,7 +202,7 @@ def repo_root(start: Path) -> Optional[Path]:
|
|
|
199
202
|
|
|
200
203
|
def find_copies(repo: Path) -> List[Path]:
|
|
201
204
|
listed = git(repo, "ls-files", "-z", "--cached", "--others", "--exclude-standard", "--",
|
|
202
|
-
"*fabric-dashboard.css", "*fabric-dashboard.js")
|
|
205
|
+
"*fabric-dashboard.css", "*fabric-dashboard.js", "*fabric-button.js")
|
|
203
206
|
if listed is not None:
|
|
204
207
|
paths = [repo / p for p in listed.split("\0") if p]
|
|
205
208
|
else:
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-service.mjs
CHANGED
|
@@ -14,7 +14,7 @@ export const EXTENSION_KEY = 'https://fabric.passioncode.ai/agent-contract/exten
|
|
|
14
14
|
// The embedded dashboard declaration (fabric-dashboard/0.1, DEC-0036 ED-9), under the well-known
|
|
15
15
|
// document's `extensions`. The kit is assets/dashboard-kit; its version is the one in kit.json.
|
|
16
16
|
export const DASHBOARD_EXTENSION_KEY = 'https://fabric.passioncode.ai/agent-contract/extensions/dashboard/0.1';
|
|
17
|
-
export const DASHBOARD_KIT = 'fabric-dashboard-kit@1.
|
|
17
|
+
export const DASHBOARD_KIT = 'fabric-dashboard-kit@1.1.0';
|
|
18
18
|
export const REFERENCE_THEMES = ['graphite', 'fabric', 'paper', 'forest', 'ocean', 'ember', 'contrast'];
|
|
19
19
|
export const EXIT_ALREADY_RUNNING = 75;
|
|
20
20
|
// Lifecycle contract (fabric-workspace knowledge/lifecycle.md), the same values as fabric_service.py.
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_service.py
CHANGED
|
@@ -43,7 +43,7 @@ EXTENSION_KEY = "https://fabric.passioncode.ai/agent-contract/extensions/service
|
|
|
43
43
|
# The embedded dashboard declaration (fabric-dashboard/0.1, DEC-0036 ED-9), under the well-known
|
|
44
44
|
# document's `extensions`. The kit is assets/dashboard-kit; its version is the one in kit.json.
|
|
45
45
|
DASHBOARD_EXTENSION_KEY = "https://fabric.passioncode.ai/agent-contract/extensions/dashboard/0.1"
|
|
46
|
-
DASHBOARD_KIT = "fabric-dashboard-kit@1.
|
|
46
|
+
DASHBOARD_KIT = "fabric-dashboard-kit@1.1.0"
|
|
47
47
|
REFERENCE_THEMES = ("graphite", "fabric", "paper", "forest", "ocean", "ember", "contrast")
|
|
48
48
|
EXIT_ALREADY_RUNNING = 75
|
|
49
49
|
# Lifecycle contract (fabric-workspace knowledge/lifecycle.md). A launchd-supervised copy that finds
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/sample_service.py
CHANGED
|
@@ -33,10 +33,12 @@ VERSION = "0.1.0"
|
|
|
33
33
|
# in the well-known document only when the kit is there to serve.
|
|
34
34
|
KIT_DIR = Path(__file__).resolve().parent.parent / "assets" / "dashboard-kit"
|
|
35
35
|
KIT_FILES = {"/kit/fabric-dashboard.css": ("fabric-dashboard.css", "text/css; charset=utf-8"),
|
|
36
|
-
"/kit/fabric-dashboard.js": ("fabric-dashboard.js", "text/javascript; charset=utf-8")
|
|
36
|
+
"/kit/fabric-dashboard.js": ("fabric-dashboard.js", "text/javascript; charset=utf-8"),
|
|
37
|
+
"/kit/fabric-button.js": ("fabric-button.js", "text/javascript; charset=utf-8")} # DEC-0040, FB-7
|
|
37
38
|
PAGE = """<!doctype html><html lang="en"><head><meta charset="utf-8">
|
|
38
39
|
<meta name="viewport" content="width=device-width,initial-scale=1"><title>{name}</title>
|
|
39
40
|
<link rel="stylesheet" href="/kit/fabric-dashboard.css"><script src="/kit/fabric-dashboard.js" defer></script>
|
|
41
|
+
<script src="/kit/fabric-button.js" defer data-service="{agent}" data-service-name="{name}"></script>
|
|
40
42
|
</head><body><div class="fd-chrome"><header class="fd-header"><p class="fd-title">{name}</p>
|
|
41
43
|
<span class="fd-chip fd-s-ok"><span class="fd-dot"></span>Status: {status}</span></header>
|
|
42
44
|
<nav class="fd-nav" aria-label="Sections"><ul class="fd-nav-list"><li><a href="#events" aria-current="page">Events</a></li>
|
|
@@ -227,7 +229,7 @@ def make_handler(svc: Service):
|
|
|
227
229
|
return self._send(401, "Open this dashboard from Fabric Dashboards.", content_type="text/plain")
|
|
228
230
|
rows = "".join("<li>%s — %s</li>" % (html.escape(e["at"]), html.escape(e["text"]))
|
|
229
231
|
for e in reversed(svc.log.fetch(None, 20)))
|
|
230
|
-
return self._send(200, PAGE.format(name=html.escape(svc.name), status="ready", rows=rows),
|
|
232
|
+
return self._send(200, PAGE.format(name=html.escape(svc.name), agent=html.escape("%s.%s" % (svc.id, svc.instance)), status="ready", rows=rows),
|
|
231
233
|
content_type="text/html; charset=utf-8")
|
|
232
234
|
self._send(404, {"error": "Not found."})
|
|
233
235
|
|
|
@@ -5,9 +5,9 @@ license: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
|
|
|
5
5
|
compatibility: Requires filesystem access and Python 3.9+. Exact schema checks additionally need git, Node.js, pnpm, and the pinned fabric-agent-contract checkout (a public repository). Works without those tools in an explicitly degraded structural-check mode. Ships in one plugin with adapting-projects-to-fabric, whose scripts it reuses.
|
|
6
6
|
metadata:
|
|
7
7
|
author: PassionCode.ai
|
|
8
|
-
version: "0.
|
|
8
|
+
version: "0.15.0"
|
|
9
9
|
contract-version: "0.1.0"
|
|
10
|
-
contract-commit: "
|
|
10
|
+
contract-commit: "0cacd08b0e95b6abf131eae1206b60792138bc3c"
|
|
11
11
|
---
|
|
12
12
|
|
|
13
13
|
# Creating Fabric-compatible agents
|
|
@@ -123,7 +123,7 @@ python3 <plugin-dir>/skills/adapting-projects-to-fabric/scripts/adapt_project.py
|
|
|
123
123
|
The effect class is declared, never defaulted: ask the author what the agent does outside itself when the request
|
|
124
124
|
does not say. Cost presets are only for a run whose cost varies; their amounts come from the author or measured runs.
|
|
125
125
|
|
|
126
|
-
Pin exactly contract `0.1.0` at commit `
|
|
126
|
+
Pin exactly contract `0.1.0` at commit `0cacd08b0e95b6abf131eae1206b60792138bc3c` and
|
|
127
127
|
read the pinned guide before implementing protocol details. If this skill is installed
|
|
128
128
|
without its sibling, the scaffolder is absent: create the bundle by hand from the pinned
|
|
129
129
|
contract's `docs/guides/connecting-compatible-agents.md` and mark the structural check
|