@passioncode-ai/fabric-agent-adapter 0.14.0 → 0.16.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 +75 -0
- package/README.md +2 -2
- 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 +1 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +3 -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/profiles.md +64 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/protocol.md +1 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service.py +11 -0
- 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-profiles.mjs +196 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-secrets.mjs +86 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-service.mjs +41 -7
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_profiles.py +277 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_secrets.py +132 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_service.py +54 -9
- 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 +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,80 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.16.0 - 2026-10-12
|
|
4
|
+
|
|
5
|
+
Runner and model profiles in both kits (contract DEC-0046, Fabric ADR-0147; FAA-19). The contract's credential
|
|
6
|
+
detector (DEC-0044) is ported too. The contract pin is unchanged (`0cacd08`).
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
- **`scripts/fabric_profiles.py` and `scripts/fabric-profiles.mjs`**:
|
|
11
|
+
- `resolve_model_profile` / `resolveModelProfile` (FAC-SEM-060). The order is request, agent, binding or project,
|
|
12
|
+
then estate default. The project's `modelClasses` narrow the result, and when nothing is left the launch is
|
|
13
|
+
refused as `model-classes-empty`, with the policy named.
|
|
14
|
+
- `profile_problems` / `profileProblems`, the FAC-SEM-058…060 checks of catalogues, profile sets, runner profiles
|
|
15
|
+
and recorded resolutions.
|
|
16
|
+
- `runner_check` / `runnerCheck`, for the doctor (runners.md C2). It runs only the catalogued bare binary on `PATH`
|
|
17
|
+
with its catalogued version and `auth` argv, never through a shell, for at most 5 s.
|
|
18
|
+
- `model_usable`.
|
|
19
|
+
- `configuration`, the well-known `configuration`, and `configuration_summary`, which gives the Runner and Model
|
|
20
|
+
tiles for the dashboard.
|
|
21
|
+
- **`scripts/fabric_secrets.py` and `scripts/fabric-secrets.mjs`**: the contract's one credential detector
|
|
22
|
+
(`secret-shape.ts`), with `credential_name`, `secret_value`, `secret_argument` and `secret_url`. It is the base for
|
|
23
|
+
FAA-23 (LC-21).
|
|
24
|
+
- **`build_well_known(configuration=)` / `buildWellKnown({ configuration })`** validates the configuration and carries
|
|
25
|
+
it. `configuration_problems` / `configurationProblems` are added.
|
|
26
|
+
- **`make_usage_receipt(route=)` / `makeUsageReceipt({ route })`**: a receipt records `subscription` or `key`, and
|
|
27
|
+
the report keeps the rows of one model on two routes apart.
|
|
28
|
+
- **Probe rule `configuration.declared`.**
|
|
29
|
+
- **Vectors**: `test/fixtures/contract/profile-vectors.json` holds 17 rule cases, 35 resolutions and 128
|
|
30
|
+
credential-detector cases. Every expected answer comes from the contract's own `profile-rules.ts` and
|
|
31
|
+
`secret-shape.ts` (`make_profile_vectors.mjs`). Credential-shaped samples are made up and stored hex-encoded.
|
|
32
|
+
- Tests: 11 planted defects across both kits were each caught.
|
|
33
|
+
- **Docs**: `references/profiles.md`, a pointer in SKILL.md, and an updated `protocol.md`. The scenario eval
|
|
34
|
+
`runner-and-model-profiles` came first.
|
|
35
|
+
|
|
36
|
+
## 0.15.0 - 2026-10-12
|
|
37
|
+
|
|
38
|
+
Fabric's button in the dashboard kit (contract DEC-0040, FB-7). With it, FAA-20 is complete. The contract pin is
|
|
39
|
+
unchanged (`0cacd08`).
|
|
40
|
+
|
|
41
|
+
### Added
|
|
42
|
+
|
|
43
|
+
- **`assets/dashboard-kit/fabric-button.js`** (kit `fabric-dashboard-kit` 1.1.0) is one floating menu button that
|
|
44
|
+
opens Fabric on the service. It is driven by `data-service`, `data-service-name`, `data-project`,
|
|
45
|
+
`data-project-name` and `data-runner`, and implements FB-1 to FB-6:
|
|
46
|
+
- **Menu (FB-4):** "Chat with Fabric about …", plus "Open a coding session in …" when the page knows its project.
|
|
47
|
+
Arrow keys, Home, End and Escape work.
|
|
48
|
+
- **Placement (FB-2):** bottom-right, or bottom-left right-to-left, 16 px plus the safe area. The page keeps the
|
|
49
|
+
button's height plus 16 px free at the end of its scroll.
|
|
50
|
+
- **Look (FB-3):** the mark alone below 480 px. The accessible name is always "Open Fabric". Colours are `--ink`
|
|
51
|
+
on `--panel` with an `--edge` boundary, the pairs the themes are already measured on.
|
|
52
|
+
- **Host button (FB-6):** the page hides its own button under the host's `fabric_button` cookie `owner=host`.
|
|
53
|
+
- **Fabric absent (FB-5):** "Fabric is not installed — get it at passioncode.ai/fabric" shows when the cookie or
|
|
54
|
+
the server says Fabric is absent, or when the page is still visible and focused 1.5 s after a link was followed.
|
|
55
|
+
|
|
56
|
+
Links are built with a port of the contract's `buildFabricLink` and `parseFabricLink`, with the FL-4 credential
|
|
57
|
+
detector. A value the grammar refuses means no button, never a bad link. A press only opens a view (FB-8). The
|
|
58
|
+
script is CSP-clean, and the only address it names is passioncode.ai/fabric.
|
|
59
|
+
- `test/fixtures/contract/fabric-links.json` holds 42 parse and 9 build cases. Every expected answer comes from the
|
|
60
|
+
contract's own functions at the pin, through `make_link_vectors.mjs`.
|
|
61
|
+
- `test/fabric-button.test.mjs` covers:
|
|
62
|
+
- the grammar against those vectors;
|
|
63
|
+
- the cookie and the menu items;
|
|
64
|
+
- the CSP check;
|
|
65
|
+
- the behaviour in WebKit through Playwright: placement, size, scroll clearance, keyboard, right-to-left, the
|
|
66
|
+
host cookie, Fabric absent, and the 1.5 s test. These are skipped when Playwright is absent.
|
|
67
|
+
|
|
68
|
+
Eight planted defects were each caught.
|
|
69
|
+
- The sample service's dashboard shows the button. The layout probe passes all 17 rules on it in WebKit.
|
|
70
|
+
- `references/dashboard-kit.md` has a "Fabric's button" section. The scenario eval `fabric-button-on-a-dashboard`
|
|
71
|
+
came first.
|
|
72
|
+
|
|
73
|
+
### Changed
|
|
74
|
+
|
|
75
|
+
- The dashboard kit is 1.1.0. `dashboard_kit.py install` also copies `fabric-button.js`, and `build` digests every
|
|
76
|
+
copied file. `DASHBOARD_KIT` in both service kits names `fabric-dashboard-kit@1.1.0`.
|
|
77
|
+
|
|
3
78
|
## 0.14.0 - 2026-10-12
|
|
4
79
|
|
|
5
80
|
The contract pin moves to Fabric Agent Contract `main` `0cacd08`, which includes PR #46 (DEC-0046, runner and model
|
package/README.md
CHANGED
|
@@ -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.16.0. 0.15.0 is the latest published GitHub/npm release (2026-10-12; 0.7.0 and 0.11.0
|
|
234
|
+
were never released); 0.16.0 is published by its `v0.16.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.16.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.16.0",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "PassionCode.ai",
|
|
9
9
|
"url": "https://passioncode.ai/"
|
|
@@ -5,7 +5,7 @@ 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.16.0"
|
|
9
9
|
contract-version: "0.1.0"
|
|
10
10
|
contract-commit: "0cacd08b0e95b6abf131eae1206b60792138bc3c"
|
|
11
11
|
---
|
|
@@ -16,7 +16,7 @@ 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.16.0"
|
|
20
20
|
contract-version: "0.1.0"
|
|
21
21
|
extension: "fabric-service/0.1"
|
|
22
22
|
extension-commit: "0cacd08b0e95b6abf131eae1206b60792138bc3c"
|
|
@@ -83,7 +83,8 @@ are in [`scripts/fabric_interop.py`](scripts/fabric_interop.py) and
|
|
|
83
83
|
[`scripts/fabric-interop.mjs`](scripts/fabric-interop.mjs). When the callers are agents of the estate, the
|
|
84
84
|
service checks each call's delegation token (DEC-0039): read [the delegation reference](references/delegation.md);
|
|
85
85
|
the verifier is [`scripts/fabric_delegation.py`](scripts/fabric_delegation.py) /
|
|
86
|
-
[`scripts/fabric-delegation.mjs`](scripts/fabric-delegation.mjs).
|
|
86
|
+
[`scripts/fabric-delegation.mjs`](scripts/fabric-delegation.mjs). When Fabric chooses the service's runner and models
|
|
87
|
+
(DEC-0046), read [the profiles reference](references/profiles.md).
|
|
87
88
|
|
|
88
89
|
## Step 2 — the non-negotiables
|
|
89
90
|
|
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.
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# Runner and model profiles (DEC-0046)
|
|
2
|
+
|
|
3
|
+
Which coding agent runs a job (the **runner**) and which models it may use (the **model profile**) are chosen in
|
|
4
|
+
Fabric, not in the service. The service resolves them in the contract's order, keeps to the project's model classes,
|
|
5
|
+
says what applies in its well-known document, and checks in its doctor that the runner and the model are usable.
|
|
6
|
+
Contract: `docs/specification/runners.md` §"Runner and model profiles" and `src/profile-rules.ts` at the pin.
|
|
7
|
+
Python: [`scripts/fabric_profiles.py`](../scripts/fabric_profiles.py); Node:
|
|
8
|
+
[`scripts/fabric-profiles.mjs`](../scripts/fabric-profiles.mjs). Both answer the contract's own vectors.
|
|
9
|
+
|
|
10
|
+
## Contents
|
|
11
|
+
|
|
12
|
+
- Resolving the model profile
|
|
13
|
+
- Checking the documents
|
|
14
|
+
- Declaring and showing what applies
|
|
15
|
+
- The doctor
|
|
16
|
+
- Usage by route
|
|
17
|
+
|
|
18
|
+
## Resolving the model profile
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
import fabric_profiles as fp
|
|
22
|
+
resolution = fp.resolve_model_profile(catalogue=catalogue, profiles=estate_and_agent_profiles,
|
|
23
|
+
choices={"request": asked, "agent": own_default, "binding": bound, "estate": estate_default},
|
|
24
|
+
policy=project_spend_policy)
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The first level that names a profile wins: **request, agent, binding or project, estate default**. The project's
|
|
28
|
+
`modelClasses` narrow the profile's models — a model stays only if every class the catalogue gives it is allowed — and
|
|
29
|
+
never choose one. An empty result is `{"refused": "model-classes-empty", "policy": {id, revision}}`: refuse the launch
|
|
30
|
+
and name the policy; never fall back to a model outside it. No level at all is `no-profile`; an id nobody offers is
|
|
31
|
+
`unknown-profile`.
|
|
32
|
+
|
|
33
|
+
## Checking the documents
|
|
34
|
+
|
|
35
|
+
`profile_problems(kind, doc)` / `profileProblems(kind, doc)` returns the contract's findings for a `model-catalogue`
|
|
36
|
+
(a model catalogued once; a provider never both ordered and ignored, a base slug covering its variants), a
|
|
37
|
+
`model-profiles` set (an id once), a `runner-profile` (a subscription only in its vendor's runner — Claude in Claude
|
|
38
|
+
Code, Kimi in Kimi Code, ChatGPT in Codex; a key route's allow-list is its own model profile; a key named by reference,
|
|
39
|
+
never by value) and a recorded `profile-resolution` (it equals the order's answer).
|
|
40
|
+
|
|
41
|
+
## Declaring and showing what applies
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
conf = fp.configuration(["runner", "model"], resolution, runner_profile)
|
|
45
|
+
well_known = fs.build_well_known(..., configuration=conf, summary=fp.configuration_summary(conf) + other_tiles)
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
`configuration` is read-only: a change is made in Fabric and reaches the service through its binding, never through
|
|
49
|
+
the well-known document. `configuration_summary` gives the **Runner** and **Model** tiles ("claude-code ·
|
|
50
|
+
subscription", "default · deepseek/deepseek-v4-pro") that the dashboard shows beside the others. The probe checks the
|
|
51
|
+
declaration (`configuration.declared`).
|
|
52
|
+
|
|
53
|
+
## The doctor
|
|
54
|
+
|
|
55
|
+
`runner_check(entry)` / `runnerCheck(entry)` takes one entry of Fabric's runner catalogue (`registry/runners.json`)
|
|
56
|
+
and answers whether the runner is installed, which version answers and whether it is signed in. It runs only a bare
|
|
57
|
+
catalogued binary found on `PATH`, only with the catalogued version argv (and `auth` argv), without a shell, within
|
|
58
|
+
the entry's timeout (at most 5 s); an entry outside these rules is refused and nothing runs. `model_usable(model,
|
|
59
|
+
catalogue, policy)` names why a model cannot be used here: not catalogued, or a class the project does not allow.
|
|
60
|
+
|
|
61
|
+
## Usage by route
|
|
62
|
+
|
|
63
|
+
`make_usage_receipt(..., route="subscription"|"key")` records how the call was paid — the operator's own login in its
|
|
64
|
+
vendor's runner, or an issued key. The usage report keeps the rows of one model on two routes apart.
|