@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.
Files changed (19) hide show
  1. package/CHANGELOG.md +62 -0
  2. package/README.md +3 -3
  3. package/package.json +1 -1
  4. package/plugins/fabric-agent-adapter/.claude-plugin/plugin.json +1 -1
  5. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/SKILL.md +3 -3
  6. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/adapt_project.py +2 -2
  7. package/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +2 -2
  8. package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/demo.html +1 -0
  9. package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-button.js +280 -0
  10. package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-dashboard.css +30 -1
  11. package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-dashboard.js +2 -2
  12. package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/kit.json +9 -3
  13. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard-kit.md +31 -2
  14. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/protocol.md +5 -2
  15. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/dashboard_kit.py +7 -4
  16. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-service.mjs +1 -1
  17. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_service.py +1 -1
  18. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/sample_service.py +4 -2
  19. 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: `2eb91c00e852d9c31d804310d38fad91589fcbac`
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.13.0. 0.12.0 is the latest published GitHub/npm release (2026-10-12; 0.7.0 and 0.11.0
234
- were never released); 0.13.0 is published by its `v0.13.0` tag. Source delivery does not update installed skills or live sessions.
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.13.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.13.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.13.0"
8
+ version: "0.15.0"
9
9
  contract-version: "0.1.0"
10
- contract-commit: "2eb91c00e852d9c31d804310d38fad91589fcbac"
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 `2eb91c00e852d9c31d804310d38fad91589fcbac`.
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
@@ -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 = "2eb91c00e852d9c31d804310d38fad91589fcbac"
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.13.0"
19
+ version: "0.15.0"
20
20
  contract-version: "0.1.0"
21
21
  extension: "fabric-service/0.1"
22
- extension-commit: "2eb91c00e852d9c31d804310d38fad91589fcbac"
22
+ extension-commit: "0cacd08b0e95b6abf131eae1206b60792138bc3c"
23
23
  ---
24
24
 
25
25
  # Building Fabric services
@@ -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.0.0 · fabric-dashboard/0.1 (Fabric Agent Contract DEC-0036)
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.0.0 · fabric-dashboard/0.1 (Fabric Agent Contract DEC-0036)
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.0.0';
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 */
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fabric-dashboard-kit",
3
- "version": "1.0.0",
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": "3993e6ab5e58d5229bc4dc245aa634fcf88a4f59502e1de90d3d3cfef70528a2",
19
- "fabric-dashboard.js": "bac615a40c85d8c668588528f9e21dac800aa01fefb56b8ee573c30520ce74a7",
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
  }
@@ -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 two files from `assets/dashboard-kit/` by hand and compare 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 two files as static assets; keep them unbundled
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 `2eb91c00e852d9c31d804310d38fad91589fcbac`
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`
@@ -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
- for text, name in ((new_css, css_path.name), (new_js, js_path.name)):
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((new_css if name.endswith(".css") else new_js).encode("utf-8")).hexdigest() for name in COPIED}
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:
@@ -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.0.0';
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.
@@ -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.0.0"
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
@@ -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.13.0"
8
+ version: "0.15.0"
9
9
  contract-version: "0.1.0"
10
- contract-commit: "2eb91c00e852d9c31d804310d38fad91589fcbac"
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 `2eb91c00e852d9c31d804310d38fad91589fcbac` and
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