@passioncode-ai/fabric-agent-adapter 0.39.1 → 0.41.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 +203 -0
- package/README.md +18 -6
- package/SECURITY.md +27 -6
- package/bin/fabric-agent-adapter.js +71 -6
- package/package.json +2 -2
- package/plugins/fabric-agent-adapter/.claude-plugin/plugin.json +1 -1
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/SKILL.md +42 -66
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/dashboard-links.md +4 -2
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/operator-channel-and-repository.md +55 -0
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/provider-entry.md +9 -3
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/verification.md +15 -0
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/adapt_project.py +181 -26
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/check_dashboard_link.py +66 -5
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_provider.py +81 -23
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_secrets.py +1 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +42 -33
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-button.js +21 -9
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-dashboard.css +1 -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 -4
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/README.md +104 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/config.example.json +15 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/knowledge.example.json +13 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/support_agent.py +465 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/board.md +21 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/connectors.md +93 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard-kit.md +2 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard-links.md +4 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/delegation.md +3 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/estate-record.md +5 -4
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/interop.md +5 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/kit-modules.md +129 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/lifecycle.md +3 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/package.md +5 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/usage.md +2 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_dashboard_layout.mjs +48 -19
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_dashboard_link.py +66 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service.py +11 -184
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service_rules.py +204 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/dashboard_kit.py +53 -19
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-auth.mjs +288 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-backups.mjs +327 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-base.mjs +47 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-fixture.mjs +79 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-link.mjs +145 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-session.mjs +848 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-shapes.mjs +340 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board.mjs +42 -1237
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-ceremonies.mjs +7 -18
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-crypto.mjs +41 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-delegation.mjs +15 -46
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-descriptor.mjs +387 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-estate.mjs +11 -68
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-events.mjs +52 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-http.mjs +231 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-interop.mjs +50 -16
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-jsvalue.mjs +67 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-lock.mjs +235 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-logs.mjs +136 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-openrouter.mjs +335 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-package.mjs +146 -32
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-paths.mjs +69 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-platform.mjs +164 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-profiles.mjs +1 -32
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-protocols.mjs +122 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-service.mjs +177 -2541
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-share.mjs +1 -8
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-supervisors.mjs +404 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-time.mjs +46 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-usage.mjs +458 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_auth.py +408 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_backups.py +399 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_base.py +91 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board.py +64 -1398
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_fixture.py +116 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_link.py +162 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_session.py +882 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_shapes.py +457 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_ceremonies.py +36 -39
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_crypto.py +336 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_delegation.py +113 -222
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_descriptor.py +586 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_estate.py +44 -405
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_events.py +140 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_http.py +402 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_hub.py +6 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_interop.py +119 -77
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_jsvalue.py +310 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_kit.py +243 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_lock.py +308 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_logs.py +202 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_openrouter.py +431 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_package.py +285 -109
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_paths.py +102 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_platform.py +338 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_profiles.py +32 -39
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_protocols.py +173 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_secrets.py +1 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_service.py +298 -3450
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_share.py +8 -8
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_supervisors.py +437 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_time.py +120 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_usage.py +697 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/kit.json +1621 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/sample_service.py +25 -7
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +29 -60
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/dashboard-links.md +4 -2
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/models-and-providers.md +3 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/operator-channel-and-repository.md +55 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/scripts/check_dashboard_link.py +66 -5
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/SKILL.md +3 -3
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/native-host-hooks.md +2 -1
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/session-hooks.md +26 -13
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session.py +122 -1392
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_cli.py +278 -0
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_env.py +539 -0
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_hosts.py +324 -0
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_mail.py +173 -0
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_mcp.py +491 -0
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/host_hooks.py +11 -1
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/hosts/context.mjs +25 -2
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/hosts/openclaw/index.mjs +4 -2
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/hosts/opencode/fabric.mjs +2 -1
package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/delegation.md
CHANGED
|
@@ -30,7 +30,7 @@ the declaration (`auth.delegation-declared`) and that `/mcp` refuses the descrip
|
|
|
30
30
|
|
|
31
31
|
```python
|
|
32
32
|
import fabric_delegation as fd, fabric_interop as fi
|
|
33
|
-
keys = fd.JwksCache(well_known["auth"]["delegation"]["jwks"]) # https, loopback http, or a file
|
|
33
|
+
keys = fd.JwksCache(well_known["auth"]["delegation"]["jwks"]) # https, loopback http, or a file; one 5 s deadline per fetch
|
|
34
34
|
gate = fd.delegation_gate(audience="example-agent", jwks=keys, issuer=well_known["auth"]["delegation"]["issuer"], # required
|
|
35
35
|
effects={"example.write": "draft", "example.publish": "publish"}) # each tool's manifest effect
|
|
36
36
|
server = fi.McpToolServer("example-agent", VERSION, authorize=gate) # pass the request headers to handle()
|
|
@@ -115,7 +115,8 @@ current verified revision as `estate=` / `{ estate }`, as a value or as a functi
|
|
|
115
115
|
- A revision that cannot be read, is outside its window, is lower than the applied one or forks it is the trust
|
|
116
116
|
source's fault: `issuer-unavailable`, detail `estate-unreadable`, `estate-outside-window`, `estate-rolled-back` or
|
|
117
117
|
`estate-forked`.
|
|
118
|
-
- Without `estate
|
|
118
|
+
- Without `estate` (the option left out), nothing changes. An explicit `null` (Python `None`) for `estate`, its
|
|
119
|
+
applied mark or its revocations is an unreadable source, `estate-unreadable`, in both kits (FAA-78).
|
|
119
120
|
- Every verifier refuses a clock that is not a number of seconds `unknown-state` `no-clock` before row 1: a NaN clock
|
|
120
121
|
would make every time row false and pass an expired token.
|
|
121
122
|
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/estate-record.md
CHANGED
|
@@ -87,8 +87,8 @@ gate = fd.delegation_gate(audience=..., jwks=keys, issuer=..., effects=..., esta
|
|
|
87
87
|
`estate-forked` (the applied number with another digest). A caller cannot cure these by minting again.
|
|
88
88
|
- An unreadable clock (not a finite number of seconds a `Date` can hold) is refused `unknown-state` `no-clock`
|
|
89
89
|
before row 1, in every verifier.
|
|
90
|
-
- Only an absent `estate` skips the check
|
|
91
|
-
|
|
90
|
+
- Only an absent `estate` skips the check: an option left out. An explicit `null` (Python `None`) is an unreadable
|
|
91
|
+
source in both kits, and so is `applied: null`; an omitted `applied` means no revision applied yet (FAA-78).
|
|
92
92
|
- **What you pass in.** Pass only a revision that `verify_estate_record` accepted and whose number is not lower than
|
|
93
93
|
the highest the machine applied; a higher one only when you verified its chain from the applied revision.
|
|
94
94
|
- `hub_key_certified(record, kid, x, at, applied, extra_revocations)` /
|
|
@@ -120,8 +120,9 @@ of `[A-Za-z0-9][A-Za-z0-9_-]{0,127}`. Absent is `personal`; `null` is not absent
|
|
|
120
120
|
`publisherKeys` pins. A key no scope names keeps DEC-0051's meaning, trusted in any context.
|
|
121
121
|
- **Update a copied kit before a revision carries `publisherScopes`.** A service runs the copy of the kit it was built
|
|
122
122
|
with; a copy older than 0.39.0 does not read `publisherScopes`, so it trusts a partner's key in every context and
|
|
123
|
-
finds no rule broken. A service that applies the estate record updates its copy
|
|
124
|
-
`fabric-estate.
|
|
123
|
+
finds no rule broken. A service that applies the estate record updates its copy first:
|
|
124
|
+
`fabric_kit.py install <dir> --module fabric_estate` (or `--lang node --module fabric-estate`). Since 0.40.0 the
|
|
125
|
+
estate reader imports the kit's core modules, so the one file alone no longer loads ([kit modules](kit-modules.md)).
|
|
125
126
|
|
|
126
127
|
Every rule is total on hostile JSON, as the contract's are: an `instancePath` names an item by its index as written (a
|
|
127
128
|
non-object before it does not shift it), and a value whose own `toString` member is data is shown as its JSON. Both kits
|
|
@@ -103,6 +103,11 @@ As a provider:
|
|
|
103
103
|
caller only. A key that already names a job starts nothing; `jobs.repeat(caller, key, now)` answers `new`, `same`
|
|
104
104
|
(answer with that job) or `run`: a `quota-exhausted` job runs again once its reset passed or you pass
|
|
105
105
|
`limit_holds=False`, a `limit-reached` job once `approval_granted=True`, and an `outcome-unknown` job never.
|
|
106
|
+
A key admits only the intent it was first used for: the key file keeps a digest of the capability and its input,
|
|
107
|
+
and the same key with another intent is refused as `idempotency-conflict` (FAA-77). `repeat` takes the intent too
|
|
108
|
+
(`capability=`, `arguments=`; Node `{capability, input}`) and then answers `conflict` for another intent. A key file
|
|
109
|
+
written before 0.41.0 holds no intent: it is answered with its job, never a conflict, and its job runs again only
|
|
110
|
+
for the capability and input that job stored.
|
|
106
111
|
- **Usage you did not measure is `null`, never 0,** but only toward a caller whose request `_meta.protocols` names
|
|
107
112
|
`fabric-interop/0.2`: pass `request_meta` to `result_envelope` (`usage_toward`). Toward any other caller the kit
|
|
108
113
|
writes the measured lower bound, as before. Either way, record that call's usage receipt with
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/kit-modules.md
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# The service kit: modules, layers and installing them
|
|
2
|
+
|
|
3
|
+
Since 0.40.0 (FAA-65) the kit is a set of small modules in two languages, not two large files. `scripts/kit.json`
|
|
4
|
+
lists every module in dependency order, per language, with the kit modules it imports and its sha256; `history` keeps
|
|
5
|
+
the digests of every release since 0.8.0, so a copy in a service can be told apart. `scripts/fabric_kit.py` installs a
|
|
6
|
+
module with exactly what it needs and reports a copy's state. Tests: `test/test_fabric_kit.py`,
|
|
7
|
+
`test/test_kit_api.py`, `test/kit-api.test.mjs`.
|
|
8
|
+
|
|
9
|
+
## Contents
|
|
10
|
+
|
|
11
|
+
- [Install, status, build](#install-status-build)
|
|
12
|
+
- [The layers](#the-layers)
|
|
13
|
+
- [What 0.39 code keeps](#what-039-code-keeps)
|
|
14
|
+
- [Dates and times](#dates-and-times)
|
|
15
|
+
- [Adding or changing a module](#adding-or-changing-a-module)
|
|
16
|
+
|
|
17
|
+
## Install, status, build
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
K=<adapter>/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts
|
|
21
|
+
python3 $K/fabric_kit.py install <service-dir> # fabric_service and its closure (Python)
|
|
22
|
+
python3 $K/fabric_kit.py install <service-dir> --module fabric_service --module fabric_interop --module fabric_board
|
|
23
|
+
# what sample_service.py imports
|
|
24
|
+
python3 $K/fabric_kit.py install <service-dir> --lang node # fabric-service.mjs and its closure
|
|
25
|
+
python3 $K/fabric_kit.py install <service-dir> --module fabric_openrouter --module fabric_board
|
|
26
|
+
python3 $K/fabric_kit.py install <service-dir> --all --dry-run # every module, nothing written
|
|
27
|
+
python3 $K/fabric_kit.py status <service-dir> [--json] # current, older (the release), modified, missing
|
|
28
|
+
python3 $K/fabric_kit.py build [--check] # maintainers: refresh kit.json from the sources
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
- `install` copies the closure of the named modules and nothing else. It refuses to overwrite a file it does not
|
|
32
|
+
recognise (`modified`: someone edited the copy) unless `--force`, and says which.
|
|
33
|
+
- `status` names each kit file in the folder as `current`, `older` with the release it came from, or `modified`, and
|
|
34
|
+
prints `missing <file>, which <file> imports` for every module the folder lacks, read from what each copy itself
|
|
35
|
+
imports and followed through the modules it would install: that copy would fail when that code path runs. A folder
|
|
36
|
+
holding only the 0.40.0 `fabric_service.py` gets one line for each of the 16 modules it needs; a 0.39.1 copy is
|
|
37
|
+
asked only for what it imports (`test_an_older_copy_is_judged_by_its_own_imports`).
|
|
38
|
+
- Exit codes: 0 done or every copy current, 1 a problem (a copy missing, `older` or `modified`, no kit module in the
|
|
39
|
+
folder at all, an install refused), 2 usage (an unknown `--module` included).
|
|
40
|
+
- Every kit module puts its own folder on `sys.path` before it imports a sibling, so a service that loads a kit file
|
|
41
|
+
by its path from another working directory gets its siblings too (`test_every_module_loads_by_path_from_any_folder`).
|
|
42
|
+
- `build --check` is what CI runs: `kit.json` must be what the sources say.
|
|
43
|
+
|
|
44
|
+
## The layers
|
|
45
|
+
|
|
46
|
+
A module imports only modules listed before it in `kit.json`, so the kit has no import cycle and no upward import
|
|
47
|
+
(`test_the_order_is_a_layering`; a planted cycle is caught). Python name first, the Node twin is the same name with
|
|
48
|
+
`-` for `_` and `.mjs`. No module is over 1000 lines (`test_no_module_over_the_line_ceiling`).
|
|
49
|
+
|
|
50
|
+
| Layer | Module | What it holds |
|
|
51
|
+
|---|---|---|
|
|
52
|
+
| L0 | `fabric_jsvalue` | JavaScript's view of JSON values: numbers, strings, equality, `jcs`, `show`; defined once for the kit |
|
|
53
|
+
| L0 | `fabric_time` | strict RFC 3339, UTC days, formatting, day windows, the clock ([dates](#dates-and-times)) |
|
|
54
|
+
| L0 | `fabric_identifiers` | identifier equality by normal form (DEC-0076) |
|
|
55
|
+
| L0 | `fabric_secrets` | the contract's one credential detector (DEC-0044) |
|
|
56
|
+
| L1 | `fabric_crypto` | Ed25519 signing (a Montgomery ladder that does not branch on the key) and verification, P-256 verification, base64url, sha256 |
|
|
57
|
+
| L2 | `fabric_estate` | `estate-record/1` revisions and their rules (DEC-0055) |
|
|
58
|
+
| L2 | `fabric_delegation` | the callee's side of `fabric-delegation/0.1` (DEC-0039) |
|
|
59
|
+
| L2 | `fabric_share` | share grants and registrations (DEC-0086) |
|
|
60
|
+
| L2 | `fabric_ceremonies` | the headless join token (DEC-0084) |
|
|
61
|
+
| L2 | `fabric_package` | `fabric-package/1`: pack, sign, verify (DEC-0050, DEC-0051) |
|
|
62
|
+
| L3 | `fabric_base` | the protocol name, `ServiceError`, small shared helpers |
|
|
63
|
+
| L3 | `fabric_platform` | Windows: named pipes, owner-only files (DEC-0032) |
|
|
64
|
+
| L3 | `fabric_paths` | the services directory, a service's folders, atomic and private writes |
|
|
65
|
+
| L3 | `fabric_protocols` | protocol revisions (DEC-0067) and feed data (DEC-0070) |
|
|
66
|
+
| L3 | `fabric_lock` | the instance lock and the supervisor marker (LC-01, DEC-0057) |
|
|
67
|
+
| L3 | `fabric_usage` | usage receipts, the ledger, the usage report, attribution, spending limits ([usage](usage.md)) |
|
|
68
|
+
| L3 | `fabric_backups` | settings backups (DEC-0025) |
|
|
69
|
+
| L3 | `fabric_descriptor` | the descriptor, peers, app descriptors, the well-known document |
|
|
70
|
+
| L3 | `fabric_auth` | the token, the network guard, login codes |
|
|
71
|
+
| L3 | `fabric_events` | events, paging, the JSONL event log (DEC-0029) |
|
|
72
|
+
| L3 | `fabric_logs` | rotating logs, the stop signal and the drain (LC-12) |
|
|
73
|
+
| L3 | `fabric_supervisors` | launchd, systemd user units, Task Scheduler (DEC-0032, LC-14) |
|
|
74
|
+
| L3 | `fabric_service` | the facade: every name 0.39.1 exported, re-exported from the modules above |
|
|
75
|
+
| L3 | `fabric_profiles` | runner and model profiles ([profiles](profiles.md)) |
|
|
76
|
+
| L3 | `fabric_interop` | `fabric-interop/0.1`: jobs, envelopes, MCP tools ([interop](interop.md)) |
|
|
77
|
+
| L4 | `fabric_hub` | the Fabric hub's door (`fabric-hub/0.1`) |
|
|
78
|
+
| L4 | `fabric_board_shapes` | the project board's wire: constants, refusal codes, shapes |
|
|
79
|
+
| L4 | `fabric_board_link` | the board's link: the MCP session over the hub, the fsynced checkpoint, a claimed request as data |
|
|
80
|
+
| L4 | `fabric_board_session` | the board responder: the work context, the fenced request, transfer and recovery |
|
|
81
|
+
| L4 | `fabric_board_fixture` | the scripted board the probe runs a responder against |
|
|
82
|
+
| L4 | `fabric_board` | the facade of the board connector ([board](board.md)) |
|
|
83
|
+
| L4 | `fabric_http` | outbound HTTP: deadline, bounded retries, `Retry-After`, size cap ([connectors](connectors.md)) |
|
|
84
|
+
| L4 | `fabric_openrouter` | OpenRouter chat: own key, spend ceiling, receipts, fallback ([connectors](connectors.md)) |
|
|
85
|
+
|
|
86
|
+
Beside the kit, not in `kit.json`: the probe (`check_service.py`, `check_service_rules.py`), the sample
|
|
87
|
+
(`sample_service.py`, `sample-remote-service.mjs`), `fabric_kit.py` itself and the dashboard tools. Recipes built on
|
|
88
|
+
the kit live in `../recipes/` ([the support agent](../recipes/support-agent/README.md)).
|
|
89
|
+
|
|
90
|
+
## What 0.39 code keeps
|
|
91
|
+
|
|
92
|
+
`import fabric_service as fs` and `import ... from './fabric-service.mjs'` keep working: the facades re-export every
|
|
93
|
+
public name 0.39.1 had, in both languages (the snapshot is `test/fixtures/kit-api/kit-api-0.39.1.json`; a missing
|
|
94
|
+
name fails `test_kit_api`), and the Python facade the private names other code reached through it (`_number`,
|
|
95
|
+
`_DAY_MS`, `_utc_date_ms` from the core). `fabric_board` and `fabric-board.mjs` are
|
|
96
|
+
facades the same way. What changed for a service that copies the kit: `fabric_service.py` alone is no longer enough;
|
|
97
|
+
install with `fabric_kit.py install` and `status` names what is missing. A test or service that replaced a function
|
|
98
|
+
by assigning it on the facade (`fabric_service.X = ...`) no longer changes what the kit does: the code reads its own
|
|
99
|
+
module's name, so patch the module that defines it (`fabric_lock.X`, `fabric_session_env.X`). A service that imported a module-private
|
|
100
|
+
helper (`_x`) of the Node twin should import it from its module now.
|
|
101
|
+
|
|
102
|
+
## Dates and times
|
|
103
|
+
|
|
104
|
+
`fabric_time` / `fabric-time.mjs` is the kit's one reading of dates, on the cases both twins test
|
|
105
|
+
(`test/fixtures/kit-time-cases.json`). Anything unreadable is NaN, never an exception, so a check compares it and
|
|
106
|
+
reports a finding.
|
|
107
|
+
|
|
108
|
+
| Python | Node | What |
|
|
109
|
+
|---|---|---|
|
|
110
|
+
| `parse_rfc3339_ms(text)` | `parseRfc3339Ms(text)` | strict RFC 3339: a `T`, an offset and a day that exists; no space, no leap second; milliseconds or NaN |
|
|
111
|
+
| `utc_date_ms(text)` | `utcDateMs(text)` | a `YYYY-MM-DD` that exists, as midnight UTC in milliseconds, or NaN |
|
|
112
|
+
| `utc_day(ms)` | `utcDay(ms)` | the UTC calendar day of an instant (NaN or Infinity raise) |
|
|
113
|
+
| `format_rfc3339(ms)` | `formatRfc3339(ms)` | `2026-10-21T09:00:00.000Z`, which parses back to the same value |
|
|
114
|
+
| `day_window(today_ms, days)` | `dayWindow(todayMs, days)` | the last N UTC days ending with today's, oldest first (a usage report's 31) |
|
|
115
|
+
| `clock_ms(at)` | `clockMs(at)` | a verifier's clock (an aware `datetime` / a `Date`, or epoch seconds) in milliseconds |
|
|
116
|
+
| `now_ms(clock)` | `nowMs(clock)` | now in milliseconds; `clock` returns seconds in Python (`time.time`), milliseconds in Node (`Date.now`) |
|
|
117
|
+
| `DAY_MS` | `DAY_MS` | 86 400 000 |
|
|
118
|
+
|
|
119
|
+
`fabric_estate.estate_time` is `parse_rfc3339_ms` under its older name.
|
|
120
|
+
|
|
121
|
+
## Adding or changing a module
|
|
122
|
+
|
|
123
|
+
1. Put it in the lowest layer whose modules it needs; import only modules listed before it.
|
|
124
|
+
2. Write the Node twin with the same public names in camelCase, and shared cases under `test/fixtures/` that both
|
|
125
|
+
test files read.
|
|
126
|
+
3. Add it to `kit.json` (module, file, layer) and run `fabric_kit.py build`; add it to the table above
|
|
127
|
+
(`test_every_kit_module_is_documented`).
|
|
128
|
+
4. A value or helper the core already has is imported, never redefined (`test_the_core_is_defined_once`).
|
|
129
|
+
5. Stay under 1000 lines; split by responsibility, keeping a facade when callers already import the old name.
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/lifecycle.md
CHANGED
|
@@ -273,7 +273,9 @@ that still exits 75 stays down until the next logon.
|
|
|
273
273
|
a named pipe on Windows (`lock_name` / `lockName`), each gone with the process. The Python kit
|
|
274
274
|
holds the same name beside its file lock, so the two kits exclude each other everywhere, and
|
|
275
275
|
`check_service.py` reports the lock held when either is. The probe only looks (`/proc/net/unix`,
|
|
276
|
-
`WaitNamedPipeW`); it never takes the name, so it cannot make a service that is starting fail.
|
|
276
|
+
`WaitNamedPipeW`); it never takes the name, so it cannot make a service that is starting fail. On Linux the Node kit's
|
|
277
|
+
name needs Node 22 or later: Node 18 and early 20 collapse every abstract socket name into one, and later 20 pads it so
|
|
278
|
+
no Python copy or probe sees it, so the Node kit refuses to start there with a sentence naming Node 22 (FAA-72).
|
|
277
279
|
|
|
278
280
|
**Token files on Windows.** The privacy of `service.token` is its ACL (service.md *Windows
|
|
279
281
|
token files*): the kits write a protected list: the user, SYSTEM and Administrators, set before the
|
|
@@ -50,7 +50,11 @@ copy. A member named twice in `fabric.package.json` is refused too.
|
|
|
50
50
|
Then every problem is listed, each with its `rule`: `schema` (the contract's schema), `FAC-SEM-064` (whole: its own
|
|
51
51
|
digest, each path once, every named file listed with the same SHA-256), `FAC-SEM-065` (no secret), `FAC-SEM-066`
|
|
52
52
|
(`agentRef` derived), `FAC-SEM-102` (one role, one server) and `folder` (`file-missing`, `file-digest`, `file-unlisted`,
|
|
53
|
-
`plugin-manifest`, `provider-manifest`)
|
|
53
|
+
`plugin-manifest`, `provider-manifest`) and `path` (a listed or folder path holding a control character, U+2028 or
|
|
54
|
+
U+2029, a `..` segment, a backslash, `//` or a colon, refused by the kit itself whatever the schema's pattern lets
|
|
55
|
+
through, FAA-78). A JSON document is read as strict UTF-8 and nested at most 256 levels; a folder entry the kit cannot
|
|
56
|
+
read is refused as `Cannot read <path>`, and a link, FIFO or other non-regular file is never followed. With a problem,
|
|
57
|
+
`--apply` writes nothing. Repacking drops old signatures and
|
|
54
58
|
says so: a changed package is signed again.
|
|
55
59
|
|
|
56
60
|
## sign
|
|
@@ -29,6 +29,8 @@ ledger.record(receipt) # fs.JsonlUsageLedger(data / "usage.j
|
|
|
29
29
|
ledger.record(k.makeUsageReceipt('openrouter', model, { inputTokens, outputTokens, costUsd: usage.cost ?? null }));
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
+
- **OpenRouter through the kit.** `fabric_openrouter.Client` records these receipts itself, one per answered call,
|
|
33
|
+
and checks a spend ceiling before every attempt ([connectors](connectors.md)).
|
|
32
34
|
- **The provider's own numbers.** Take tokens from the response's usage block, not from a local
|
|
33
35
|
estimate. The cost is `provider` when the provider reported the charge (OpenRouter `usage.cost`,
|
|
34
36
|
a billing API) and `price-list` when you computed it from a published price list.
|
|
@@ -22,6 +22,13 @@ import { pathToFileURL } from 'node:url';
|
|
|
22
22
|
|
|
23
23
|
const EXTENSION_KEY = 'https://fabric.passioncode.ai/agent-contract/extensions/dashboard/0.1';
|
|
24
24
|
const RULES = ['ED-1', 'ED-2', 'ED-3', 'ED-4', 'ED-5'];
|
|
25
|
+
// The most elements ED-4 and ED-5 walk on one page; past it they say how much they did not see.
|
|
26
|
+
const ELEMENT_CAP = 6000;
|
|
27
|
+
|
|
28
|
+
// A one-time login code is a credential: no report, error or evidence line carries one.
|
|
29
|
+
export function scrub(text) {
|
|
30
|
+
return String(text).replace(/([?&#;]code=)[^&#\s"']*/gi, '$1<redacted>');
|
|
31
|
+
}
|
|
25
32
|
|
|
26
33
|
function usage(message) {
|
|
27
34
|
if (message) process.stderr.write(message + '\n');
|
|
@@ -103,7 +110,7 @@ async function serviceTarget(spec, dir) {
|
|
|
103
110
|
}
|
|
104
111
|
|
|
105
112
|
// Runs inside the page. Everything it reports is measured from the rendered layout.
|
|
106
|
-
function measure(floor) {
|
|
113
|
+
function measure([floor, cap]) {
|
|
107
114
|
const de = document.documentElement;
|
|
108
115
|
const vw = de.clientWidth;
|
|
109
116
|
const vh = window.innerHeight;
|
|
@@ -111,6 +118,9 @@ function measure(floor) {
|
|
|
111
118
|
const cs = (el) => getComputedStyle(el);
|
|
112
119
|
const scrollsX = (el) => /(auto|scroll)/.test(cs(el).overflowX);
|
|
113
120
|
const clipsX = (el) => cs(el).overflowX !== 'visible';
|
|
121
|
+
// Vertically a scroll box (auto, scroll) still lets the reader reach what it holds; hidden and clip do not.
|
|
122
|
+
const clipsY = (el) => /(hidden|clip)/.test(cs(el).overflowY);
|
|
123
|
+
const pinned = (el) => { for (let a = el; a && a !== de; a = a.parentElement) if (cs(a).position === 'fixed') return true; return false; };
|
|
114
124
|
const label = (el) => {
|
|
115
125
|
const id = el.id ? '#' + el.id : '';
|
|
116
126
|
const cls = typeof el.className === 'string' && el.className.trim() ? '.' + el.className.trim().split(/\s+/).join('.') : '';
|
|
@@ -118,16 +128,22 @@ function measure(floor) {
|
|
|
118
128
|
};
|
|
119
129
|
const visible = (el) => { const r = el.getBoundingClientRect(); return r.width > 0 && r.height > 0 && cs(el).visibility !== 'hidden'; };
|
|
120
130
|
|
|
121
|
-
// ED-3: a link is reachable when it is fully inside the viewport
|
|
122
|
-
//
|
|
131
|
+
// ED-3: a link is reachable when it is fully inside the viewport's width, inside the page's height
|
|
132
|
+
// (the viewport's, for something fixed to it) and not clipped by an ancestor on either axis, or
|
|
133
|
+
// when it sits in a closed <details> whose summary is reachable, or in a menu a visible
|
|
123
134
|
// aria-controls button opens.
|
|
124
135
|
const inViewport = (el) => {
|
|
125
136
|
const r = el.getBoundingClientRect();
|
|
126
137
|
if (r.width <= 0 || r.height <= 0 || r.left < -1 || r.right > vw + 1) return false;
|
|
138
|
+
const [top, bottom] = pinned(el) ? [0, vh] : [-window.scrollY, de.scrollHeight - window.scrollY];
|
|
139
|
+
if (r.top < top - 1 || r.bottom > bottom + 1) return false;
|
|
127
140
|
for (let a = el.parentElement; a && a !== de; a = a.parentElement) {
|
|
128
|
-
|
|
141
|
+
const x = clipsX(a);
|
|
142
|
+
const y = clipsY(a);
|
|
143
|
+
if (!x && !y) continue;
|
|
129
144
|
const ar = a.getBoundingClientRect();
|
|
130
|
-
if (r.left < ar.left - 1 || r.right > ar.right + 1) return false;
|
|
145
|
+
if (x && (r.left < ar.left - 1 || r.right > ar.right + 1)) return false;
|
|
146
|
+
if (y && (r.top < ar.top - 1 || r.bottom > ar.bottom + 1)) return false;
|
|
131
147
|
}
|
|
132
148
|
return true;
|
|
133
149
|
};
|
|
@@ -155,9 +171,11 @@ function measure(floor) {
|
|
|
155
171
|
// unless it is inside a scroll box or clipped by an ancestor that is itself inside the viewport.
|
|
156
172
|
const all = document.body ? document.body.querySelectorAll('*') : [];
|
|
157
173
|
const FOCUSABLE = 'a[href], button, input, select, textarea, [tabindex]';
|
|
174
|
+
out.elements = all.length;
|
|
175
|
+
out.measured = Math.min(all.length, cap);
|
|
158
176
|
let n = 0;
|
|
159
177
|
for (const el of all) {
|
|
160
|
-
if (++n >
|
|
178
|
+
if (++n > cap) break;
|
|
161
179
|
if (!visible(el)) continue;
|
|
162
180
|
if (scrollsX(el) && el.scrollWidth > el.clientWidth + 1) {
|
|
163
181
|
const keyboard = el.tabIndex >= 0 || !!el.querySelector(FOCUSABLE);
|
|
@@ -214,21 +232,28 @@ export function verdicts(width, floor, m) {
|
|
|
214
232
|
else add('ED-3', m.nav.unreachable.length ? 'FAIL' : 'PASS',
|
|
215
233
|
m.nav.unreachable.length ? `${m.nav.unreachable.length} of ${m.nav.total} links unreachable: ${m.nav.unreachable.slice(0, 6).join(', ')}` : `${m.nav.total} links reachable`);
|
|
216
234
|
const badBoxes = m.boxes.filter((b) => !b.keyboard || !b.named);
|
|
235
|
+
// A page larger than the walk: what the walk saw can fail, but it cannot pass for the whole page.
|
|
236
|
+
const capped = m.elements > m.measured ? `only the first ${m.measured} of ${m.elements} elements measured` : '';
|
|
217
237
|
const problems = [];
|
|
218
238
|
if (m.past.length) problems.push(`past the viewport, not in a scroll box: ${m.past.slice(0, 4).join(', ')}`);
|
|
219
239
|
if (badBoxes.length) problems.push(`scroll box without keyboard access or a name: ${badBoxes.slice(0, 4).map((b) => b.el).join(', ')}`);
|
|
220
|
-
add('ED-4',
|
|
240
|
+
if (problems.length) add('ED-4', 'FAIL', problems.join('; ') + (capped ? `; ${capped}` : ''));
|
|
241
|
+
else if (capped) add('ED-4', 'NOT_RUN', `${capped}: ${m.boxes.length} overflowing box(es) keyboard-reachable among them, the rest not seen`);
|
|
242
|
+
else add('ED-4', 'PASS', `${m.boxes.length} overflowing box(es), each keyboard-reachable; edge shades not measured`);
|
|
221
243
|
const limit = Math.min(120, m.vh / 4);
|
|
222
244
|
const ed5 = [];
|
|
223
245
|
if (m.chrome > limit) ed5.push(`sticky chrome ${m.chrome} px > ${Math.round(limit)} px`);
|
|
224
246
|
if (m.nested.length) ed5.push(`nested full-height scroller: ${m.nested.slice(0, 3).join(', ')}`);
|
|
225
247
|
if (m.anchor && m.anchor.top < m.chrome - 1) ed5.push(`anchor ${m.anchor.el} lands at ${m.anchor.top} px, under the ${m.chrome} px chrome`);
|
|
226
248
|
add('ED-5', ed5.length ? 'FAIL' : 'PASS', ed5.join('; ') ||
|
|
227
|
-
`sticky chrome ${m.chrome} px ≤ ${Math.round(limit)} px` + (m.anchor ? `; anchor lands at ${m.anchor.top} px` : '; no in-page anchor to jump to') + '; refresh keeps scroll: not measured'
|
|
249
|
+
`sticky chrome ${m.chrome} px ≤ ${Math.round(limit)} px` + (m.anchor ? `; anchor lands at ${m.anchor.top} px` : '; no in-page anchor to jump to') + '; refresh keeps scroll: not measured' +
|
|
250
|
+
(capped ? `; nested scrollers: ${capped}` : ''));
|
|
228
251
|
return rows;
|
|
229
252
|
}
|
|
230
253
|
|
|
231
254
|
function report(rows, opts, target) {
|
|
255
|
+
rows = rows.map((r) => ({ ...r, evidence: scrub(r.evidence) }));
|
|
256
|
+
target = scrub(target);
|
|
232
257
|
const failed = rows.filter((r) => r.verdict === 'FAIL').length;
|
|
233
258
|
if (opts.json) {
|
|
234
259
|
process.stdout.write(JSON.stringify({ target, browser: opts.browser, results: rows, failed }, null, 2) + '\n');
|
|
@@ -241,27 +266,31 @@ function report(rows, opts, target) {
|
|
|
241
266
|
|
|
242
267
|
async function main(argv) {
|
|
243
268
|
const opts = parseArgs(argv);
|
|
269
|
+
// What the report names: the service, never the login URL its one-time code is in.
|
|
270
|
+
const shown = opts.service || opts.url;
|
|
244
271
|
let target = opts.url;
|
|
245
272
|
let floor = opts.floor;
|
|
246
|
-
const notRun = (why) => report(RULES.map((rule) => ({ rule, width: 0, verdict: 'NOT_RUN', evidence: why })), opts,
|
|
273
|
+
const notRun = (why) => report(RULES.map((rule) => ({ rule, width: 0, verdict: 'NOT_RUN', evidence: why })), opts, shown);
|
|
274
|
+
// The browser first: a run that cannot measure must not spend a one-time code.
|
|
275
|
+
const pw = await loadPlaywright(opts.playwright);
|
|
276
|
+
if (!pw) return notRun('Playwright is not installed here: npm i -D playwright && npx playwright install webkit (or pass --playwright DIR)');
|
|
277
|
+
let browser;
|
|
278
|
+
try {
|
|
279
|
+
browser = await pw[opts.browser].launch();
|
|
280
|
+
} catch (err) {
|
|
281
|
+
return notRun(`the ${opts.browser} browser does not start: ${String(err.message).split('\n')[0]} (npx playwright install ${opts.browser})`);
|
|
282
|
+
}
|
|
247
283
|
if (opts.service) {
|
|
248
284
|
try {
|
|
249
285
|
const t = await serviceTarget(opts.service, opts.servicesDir);
|
|
250
286
|
target = t.url;
|
|
251
287
|
if (floor === undefined && Number.isInteger(t.declared?.minWidth)) floor = t.declared.minWidth;
|
|
252
288
|
} catch (err) {
|
|
289
|
+
await browser.close();
|
|
253
290
|
return notRun(`could not open the service's dashboard: ${err.message}`);
|
|
254
291
|
}
|
|
255
292
|
}
|
|
256
293
|
floor = floor ?? 360;
|
|
257
|
-
const pw = await loadPlaywright(opts.playwright);
|
|
258
|
-
if (!pw) return notRun('Playwright is not installed here: npm i -D playwright && npx playwright install webkit (or pass --playwright DIR)');
|
|
259
|
-
let browser;
|
|
260
|
-
try {
|
|
261
|
-
browser = await pw[opts.browser].launch();
|
|
262
|
-
} catch (err) {
|
|
263
|
-
return notRun(`the ${opts.browser} browser does not start: ${String(err.message).split('\n')[0]} (npx playwright install ${opts.browser})`);
|
|
264
|
-
}
|
|
265
294
|
const rows = [];
|
|
266
295
|
try {
|
|
267
296
|
const context = await browser.newContext({ viewport: { width: Math.max(...opts.widths), height: opts.height } });
|
|
@@ -279,7 +308,7 @@ async function main(argv) {
|
|
|
279
308
|
await page.evaluate(() => new Promise((r) => requestAnimationFrame(() => requestAnimationFrame(r))));
|
|
280
309
|
await page.waitForTimeout(250);
|
|
281
310
|
await page.evaluate(() => new Promise((r) => requestAnimationFrame(() => requestAnimationFrame(r))));
|
|
282
|
-
const m = await page.evaluate(measure, floor);
|
|
311
|
+
const m = await page.evaluate(measure, [floor, ELEMENT_CAP]);
|
|
283
312
|
rows.push(...verdicts(width, floor, m));
|
|
284
313
|
}
|
|
285
314
|
} catch (err) {
|
|
@@ -287,7 +316,7 @@ async function main(argv) {
|
|
|
287
316
|
} finally {
|
|
288
317
|
await browser.close();
|
|
289
318
|
}
|
|
290
|
-
return report(rows, opts,
|
|
319
|
+
return report(rows, opts, shown);
|
|
291
320
|
}
|
|
292
321
|
|
|
293
322
|
if (import.meta.url === pathToFileURL(process.argv[1] || '').href) {
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_dashboard_link.py
CHANGED
|
@@ -7,7 +7,7 @@ This is an output correctness gate, not authentication, discovery, or a relay.
|
|
|
7
7
|
import json
|
|
8
8
|
import re
|
|
9
9
|
import sys
|
|
10
|
-
from urllib.parse import parse_qs, urlsplit
|
|
10
|
+
from urllib.parse import parse_qs, parse_qsl, unquote, urlsplit
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
def require(condition, reason):
|
|
@@ -15,6 +15,63 @@ def require(condition, reason):
|
|
|
15
15
|
raise ValueError(reason)
|
|
16
16
|
|
|
17
17
|
|
|
18
|
+
# Credential names and values: the same rules as fabric-button.js (credentialName, secretValue).
|
|
19
|
+
CREDENTIAL_WORDS = {
|
|
20
|
+
'token', 'secret', 'password', 'passwd', 'pass', 'passphrase', 'credential', 'credentials', 'cookie',
|
|
21
|
+
'authorization', 'bearer', 'jwt', 'otp', 'apikey', 'signature', 'sig', 'accesstoken', 'idtoken', 'authtoken',
|
|
22
|
+
'refreshtoken', 'sessionid', 'sessiontoken', 'logincode', 'authcode', 'privatekey', 'clientsecret', 'secretkey',
|
|
23
|
+
'accesskey'}
|
|
24
|
+
CREDENTIAL_PAIRS = {
|
|
25
|
+
'key': {'private', 'api', 'access', 'secret', 'session', 'auth', 'signing', 'encryption', 'master', 'client', 'login',
|
|
26
|
+
'refresh'},
|
|
27
|
+
'auth': {'token', 'code', 'header'},
|
|
28
|
+
'session': {'token', 'id'},
|
|
29
|
+
'code': {'login', 'auth', 'access', 'session'}}
|
|
30
|
+
JWT_SHAPE = re.compile(r'eyJ[A-Za-z0-9_-]{2,}\.[A-Za-z0-9_-]{2,}\.[A-Za-z0-9_-]*')
|
|
31
|
+
SECRET_VALUE = re.compile(r'(^bearer\s.+|sk-[A-Za-z0-9_-]{16,}|gh[pousr]_[A-Za-z0-9]{20,}|xox[abposr]-[A-Za-z0-9-]{10,}'
|
|
32
|
+
r'|AKIA[0-9A-Z]{16}|glpat-[A-Za-z0-9_-]{20,})', re.I | re.S)
|
|
33
|
+
LONG_RUN = re.compile(r'[A-Za-z0-9+/=_-]{40,}')
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def name_words(name):
|
|
37
|
+
name = re.sub(r'([a-z0-9])([A-Z])', r'\1 \2', name)
|
|
38
|
+
name = re.sub(r'([A-Z]+)([A-Z][a-z])', r'\1 \2', name)
|
|
39
|
+
return [w.lower() for w in re.split(r'[\s._-]+', name) if w]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def credential_name(name):
|
|
43
|
+
split = name_words(name)
|
|
44
|
+
words = set(split) | {w[:-1] for w in split if len(w) > 2 and w.endswith('s')}
|
|
45
|
+
if words & CREDENTIAL_WORDS:
|
|
46
|
+
return True
|
|
47
|
+
if len(split) == 1 and words & set(CREDENTIAL_PAIRS):
|
|
48
|
+
return True
|
|
49
|
+
return any(w in words and words & pairs for w, pairs in CREDENTIAL_PAIRS.items())
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def secret_value(value):
|
|
53
|
+
long_secret = any(re.search(r'[a-z]', run) and re.search(r'[A-Z]', run) and re.search(r'[0-9]', run)
|
|
54
|
+
for run in LONG_RUN.findall(value))
|
|
55
|
+
return bool(JWT_SHAPE.search(value) or SECRET_VALUE.search(value) or long_secret)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def carries_credential(text):
|
|
59
|
+
# A query or fragment whose names or values could be a credential: refused even with an empty value.
|
|
60
|
+
return (secret_value(text) or secret_value(unquote(text))
|
|
61
|
+
or any(credential_name(name) or secret_value(name) or secret_value(value)
|
|
62
|
+
for name, value in parse_qsl(text, keep_blank_values=True)))
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def login_path(path):
|
|
66
|
+
# The path a server routes: refuse what normalises differently in different servers, then compare.
|
|
67
|
+
decoded = unquote(path)
|
|
68
|
+
require(not re.search(r'%2f|%5c', path, re.I) and '\\' not in decoded, 'encoded_separator')
|
|
69
|
+
segments = decoded.split('/')[1:]
|
|
70
|
+
require(not any(seg in ('.', '..') for seg in segments), 'dot_segment')
|
|
71
|
+
require(all(seg for seg in segments[:-1]), 'empty_segment')
|
|
72
|
+
return '/'.join([''] + [seg for seg in segments if seg]).lower() == '/fabric/v1/login'
|
|
73
|
+
|
|
74
|
+
|
|
18
75
|
def safe_path(path):
|
|
19
76
|
return (isinstance(path, str) and path.startswith('/') and not path.startswith('//')
|
|
20
77
|
and len(path) <= 2048 and not re.search(r'[\\\x00-\x1f\x7f]', path))
|
|
@@ -42,10 +99,14 @@ def check(data):
|
|
|
42
99
|
and not plain.username and not plain.password, 'invalid_http_fallback')
|
|
43
100
|
http_page = plain.path + ('?' + plain.query if plain.query else '') + ('#' + plain.fragment if plain.fragment else '')
|
|
44
101
|
require(safe_path(http_page) and http_page == (page or '/'), 'mismatched_page')
|
|
45
|
-
sensitive = {'token', 'access_token', 'refresh_token', 'password', 'secret', 'login_code'
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
102
|
+
sensitive = {'token', 'access_token', 'refresh_token', 'password', 'secret', 'login_code', 'code', 'api_key',
|
|
103
|
+
'apikey', 'key', 'auth'}
|
|
104
|
+
parts = urlsplit(http_page)
|
|
105
|
+
places = [parts.query, parts.fragment] + parts.fragment.split('?', 1)[1:]
|
|
106
|
+
require(not any(sensitive.intersection(k.lower() for k in parse_qs(p, keep_blank_values=True)) for p in places),
|
|
107
|
+
'credential_query')
|
|
108
|
+
require(not any(carries_credential(p) for p in places), 'credential_query')
|
|
109
|
+
require(not login_path(parts.path), 'login_link_is_not_dashboard')
|
|
49
110
|
state = host.get('state')
|
|
50
111
|
require(state in ('available', 'not_installed', 'unknown', 'incompatible', 'handler_mismatch', 'unsupported'), 'unknown_host_state')
|
|
51
112
|
target, viewer = data.get('target_device'), data.get('viewer_device')
|