@passioncode-ai/fabric-agent-adapter 0.42.1 → 0.44.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/README.md +1 -1
  3. package/package.json +1 -1
  4. package/plugins/fabric-agent-adapter/.claude-plugin/plugin.json +1 -1
  5. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/SKILL.md +3 -3
  6. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/adapt_project.py +2 -2
  7. package/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +8 -3
  8. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard-kit.md +2 -1
  9. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/kit-modules.md +1 -0
  10. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/protocol.md +23 -2
  11. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_dashboard_layout.mjs +8 -5
  12. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service.py +9 -0
  13. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-backups.mjs +2 -2
  14. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-connections.mjs +186 -0
  15. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-delegation.mjs +2 -1
  16. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-descriptor.mjs +6 -0
  17. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-package.schema.json +1 -1
  18. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-protocols.mjs +1 -1
  19. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-secrets.mjs +2 -2
  20. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-service.mjs +1 -0
  21. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_backups.py +2 -2
  22. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_connections.py +448 -0
  23. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_delegation.py +3 -1
  24. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_descriptor.py +6 -0
  25. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_protocols.py +1 -1
  26. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_service.py +6 -0
  27. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/kit.json +181 -14
  28. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/share-registration.schema.json +2 -1
  29. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +3 -3
  30. package/plugins/fabric-agent-adapter/skills/working-with-fabric/SKILL.md +1 -1
  31. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_env.py +1 -1
  32. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_mcp.py +4 -3
  33. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/host_hooks.py +3 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,64 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.44.0 (2026-10-22)
4
+
5
+ ### Added
6
+
7
+ - **What an agent uses, `fabric-connections/0.1`** (FAA-62, contract DEC-0096, FAC-SEM-127; asked by Fabric Dashboards
8
+ on board thread `131648c0`). A new kit module in both twins, `fabric_connections` / `fabric-connections.mjs`:
9
+ `connections_extension(declared, degraded, states)` / `connectionsExtension(declared, {degraded, states})` builds
10
+ the `extensions` entry, each connection's `state` from the service's own checks, else `degraded` when `degraded[]`
11
+ names it, else `unknown`, and refuses a declaration a host would hide (the schema, an id twice, a credential).
12
+ `connections_view` / `connectionsView` read a peer's list as the contract's reference does; both answer all 127
13
+ cases of the contract's `fixtures/vectors/connections.json`, vendored by `make_connections_vectors.mjs`.
14
+ `build_well_known` / `buildWellKnown` refuse an invalid declaration passed in by hand, `check_service.py` reports
15
+ `well-known.connections`, and the `fabric_service` facades re-export the four names. The Python twin reads
16
+ credentials with the reference's ECMAScript semantics (ASCII case folding and word boundary, ECMAScript's whitespace,
17
+ a URL WHATWG refuses read as text only, its query and fragment read as WHATWG serializes them, a name's leading
18
+ `?` dropped as `URLSearchParams` drops it, an unpaired surrogate read as U+FFFD); 1,073 more inputs, answered by the
19
+ contract's reader into `connections-extra-vectors.json`, hold both twins to it. Inputs whose answer changes with the
20
+ Node release (the reference parses URLs with the runtime's own `new URL`) are left out of that set. Where Python cannot tell whether WHATWG refuses a URL (a
21
+ non-ASCII host), it reads the URL's credentials anyway, so a doubt hides a declaration rather than publish one
22
+ (FAA-98 ports UTS46 to close that gap).
23
+
24
+ ### Changed
25
+
26
+ - **`check_dashboard_layout.mjs` exits 3 when no rule was measured** (FAA-96): every rule `NOT_RUN` (no Playwright, a
27
+ browser that does not start, a service it cannot open) or no rule at all. It exited 0, and 2 for an empty report
28
+ since 0.42.1. 0 still needs at least one rule measured and none failing.
29
+
30
+ ### Fixed
31
+
32
+ - **The session worker waits for an exit status to its deadline after a closed pipe** (FAA-97): a `fabric-mcp` that
33
+ closed its stdin and exited 3 more than a second later was logged as `outcome=unknown` and its spool kept.
34
+ - **OpenClaw `--remove` drops our load path when its folder is gone** (FAA-90): the ownership check read a manifest
35
+ that no longer existed, so the dead path stayed.
36
+
37
+ ## 0.43.0 (2026-10-22)
38
+
39
+ ### Changed
40
+
41
+ - **Pinned to contract `bfa9a67`** (FAA-95; the contract review, PR #137 and #138, DEC-0096 and DEC-0097). `c55f2d5`
42
+ (0.39.0 to 0.42.1) joins the supported revisions. Every vendored vector was regenerated from the contract's own
43
+ functions at the pin: new negative package fixtures, a `comms-complete` case and `fabric-connections` in the register.
44
+ - **A delegation chain names a partner only for a partner actor** (contract `src/delegation.ts` at `bfa9a67`): an actor
45
+ that carries `partner` beside `sub` is named by its `sub` in the verified `chain`, in both kits.
46
+ - **`fabric-connections/0.1` is in both protocol registers** (DEC-0096), so a `protocols` list naming it is read as
47
+ the contract reads it. The kits do not emit the extension yet (FAA-62).
48
+ - **The provider schema's MCP branches are read as `if`/`then`/`else`** (the contract review): the schema parity test
49
+ takes the URL pattern from `else` (a `oneOf` before); the pattern itself is unchanged. No kit code walked that `oneOf`.
50
+
51
+ ### Fixed
52
+
53
+ - **The kit's copy of `fabric-package.schema.json` carries DEC-0097's path pattern.** ECMAScript's `.` stops at U+2028
54
+ and U+2029, so the old pattern accepted `skills<U+2028>/../../etc/passwd`; any character is now `[\s\S]`. Both kits'
55
+ own package checks already refused these paths (0.41.0). `share-registration.schema.json` is regenerated at the pin.
56
+ - **The settings backup's secret-name patterns follow DEC-0097** in both kits (`[\s\S]+_` for `.+_`); no input reached
57
+ the difference, since table and column names refuse a line break first. A contract-gated test holds each twin's
58
+ regex to the pinned schema's pattern.
59
+ - **A guard holds the kit's vendored schemas to the pin:** `test_the_kit_schemas_carry_the_patterns_of_the_pinned_contract`
60
+ fails when a pattern in either copy is not a pattern of that schema's `$ref` closure at the pinned contract.
61
+
3
62
  ## 0.42.1 (2026-10-22)
4
63
 
5
64
  The first published release of the 0.42 line: everything in 0.42.0 below, plus one test fix.
package/README.md CHANGED
@@ -274,7 +274,7 @@ Scaffolding is non-destructive by default. It writes only the paths declared in
274
274
  The default pin for new bundles is [`fabric-contract.lock.json`](fabric-contract.lock.json):
275
275
 
276
276
  - version: `0.1.0`
277
- - commit: `c55f2d5a11c9a0197e958e87212fca7ae1154769`
277
+ - commit: `bfa9a67aa3c6cb808c093e7d83cd59799f1ee7d0`
278
278
 
279
279
  Every other mention of the default contract revision (this section, the skills' metadata, the
280
280
  skill card, `adapt_project.py`) must equal it; `python3 test/validate.py` fails a tree
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@passioncode-ai/fabric-agent-adapter",
3
- "version": "0.42.1",
3
+ "version": "0.44.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; and teach a coding agent to work in a project through Fabric (fabric-mcp, fabric_whoami, claim and release), with Claude Code session hooks.",
6
- "version": "0.42.1",
6
+ "version": "0.44.0",
7
7
  "author": {
8
8
  "name": "PassionCode.ai",
9
9
  "url": "https://passioncode.ai/"
@@ -5,9 +5,9 @@ license: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
5
5
  compatibility: Requires filesystem access and Python 3.9+. Exact schema checks additionally need git, Node.js, pnpm, and the pinned fabric-agent-contract checkout (a public repository). Works without those tools in an explicitly degraded structural-check mode.
6
6
  metadata:
7
7
  author: PassionCode.ai
8
- version: "0.42.1"
8
+ version: "0.44.0"
9
9
  contract-version: "0.1.0"
10
- contract-commit: "c55f2d5a11c9a0197e958e87212fca7ae1154769"
10
+ contract-commit: "bfa9a67aa3c6cb808c093e7d83cd59799f1ee7d0"
11
11
  ---
12
12
 
13
13
  # Adapting projects to Fabric
@@ -76,7 +76,7 @@ Use exactly:
76
76
 
77
77
  - contract version `0.1.0`;
78
78
  - repository `https://github.com/passioncode-ai/fabric-agent-contract`;
79
- - commit `c55f2d5a11c9a0197e958e87212fca7ae1154769`.
79
+ - commit `bfa9a67aa3c6cb808c093e7d83cd59799f1ee7d0`.
80
80
 
81
81
  This is the default for new bundles. When checking an issued legacy bundle, preserve its
82
82
  explicit supported lock and use that exact clean checkout. Never silently upgrade or
@@ -22,10 +22,10 @@ import fabric_secrets as _secrets # noqa: E402 - the contract's credential dete
22
22
 
23
23
  CONTRACT_VERSION = "0.1.0"
24
24
  CONTRACT_REPOSITORY = "https://github.com/passioncode-ai/fabric-agent-contract"
25
- CONTRACT_COMMIT = "c55f2d5a11c9a0197e958e87212fca7ae1154769"
25
+ CONTRACT_COMMIT = "bfa9a67aa3c6cb808c093e7d83cd59799f1ee7d0"
26
26
  # #region supported-contract-revisions — docs: README.md#contract-pin
27
27
  # Only this declaration may name a legacy revision in live files.
28
- SUPPORTED_CONTRACT_COMMITS = (CONTRACT_COMMIT, "07042181bfae2a481aa0da1a5651f24c76669c48", "d8146cfb0a1c74fc6432284c4356bb0c864b37b5", "82e163665e156a08aa3b92e4711015582c06162c", "1636fa97262f4a26e2086572d540c2ee701e0010", "f00a7685e3861c27f7899c033fb850a145a1708f", "a046f57cf46ea9e3da57ff8ce6a3ed4d26d8774b", "fe55fc67cba1a9e34a736da7d148d4fe2a8fbfa0", "b291bf09e0a9f9fd3c16cf540832cd021203b7d1", "0cacd08b0e95b6abf131eae1206b60792138bc3c", "2eb91c00e852d9c31d804310d38fad91589fcbac", "8e49fbd5fbbf230e32c1969cd24ac3c22422f0a5", "a83bec6b47776af688402c18efec507143061f68", "52da526d7cc1e063d74c5b520dbc03d0ace15246", "78b2da018d2c4bd9ac1bde1dc3b7f1a110a175f5", "623bf61358c339cb10297807b3f024b5d9f1f327", "23f9fda4c05f8a3852246ee98d4f2adf74ed0875", "9091d3d6606b0c4b5591a38c3674356a01fd7f55", "df55c8c54a23251342a7ee57ba95642b7eb39e61", "2ce392291c6668598d12cd38327e24696b5ca15c")
28
+ SUPPORTED_CONTRACT_COMMITS = (CONTRACT_COMMIT, "c55f2d5a11c9a0197e958e87212fca7ae1154769", "07042181bfae2a481aa0da1a5651f24c76669c48", "d8146cfb0a1c74fc6432284c4356bb0c864b37b5", "82e163665e156a08aa3b92e4711015582c06162c", "1636fa97262f4a26e2086572d540c2ee701e0010", "f00a7685e3861c27f7899c033fb850a145a1708f", "a046f57cf46ea9e3da57ff8ce6a3ed4d26d8774b", "fe55fc67cba1a9e34a736da7d148d4fe2a8fbfa0", "b291bf09e0a9f9fd3c16cf540832cd021203b7d1", "0cacd08b0e95b6abf131eae1206b60792138bc3c", "2eb91c00e852d9c31d804310d38fad91589fcbac", "8e49fbd5fbbf230e32c1969cd24ac3c22422f0a5", "a83bec6b47776af688402c18efec507143061f68", "52da526d7cc1e063d74c5b520dbc03d0ace15246", "78b2da018d2c4bd9ac1bde1dc3b7f1a110a175f5", "623bf61358c339cb10297807b3f024b5d9f1f327", "23f9fda4c05f8a3852246ee98d4f2adf74ed0875", "9091d3d6606b0c4b5591a38c3674356a01fd7f55", "df55c8c54a23251342a7ee57ba95642b7eb39e61", "2ce392291c6668598d12cd38327e24696b5ca15c")
29
29
  # #endregion supported-contract-revisions
30
30
  INTEROP_KEY = "https://fabric.passioncode.ai/agent-contract/extensions/interop/0.1"
31
31
  MCP_REVISION = "2026-07-28"
@@ -14,10 +14,10 @@ license: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
14
14
  compatibility: Python 3.9+ or Node.js 20+ for the kits (Node.js 22+ for a Linux service's instance lock); the probe needs Python 3.9+. launchd steps are macOS-only; Linux services install a systemd user unit and Windows ones a per-user Scheduled Task with the kits' writers. Dashboard handoff optionally uses Fabric Dashboards MCP link/host_status/open; without it, report unresolved host capability. The contract checkout is optional.
15
15
  metadata:
16
16
  author: PassionCode.ai
17
- version: "0.42.1"
17
+ version: "0.44.0"
18
18
  contract-version: "0.1.0"
19
19
  extension: "fabric-service/0.1"
20
- extension-commit: "c55f2d5a11c9a0197e958e87212fca7ae1154769"
20
+ extension-commit: "bfa9a67aa3c6cb808c093e7d83cd59799f1ee7d0"
21
21
  ---
22
22
 
23
23
  # Building Fabric services
@@ -248,6 +248,11 @@ and declare it with `extensions=fs.dashboard_extension()`. Read
248
248
  declaration and how to upgrade a dashboard built before the kit: an upgrade you may make yourself,
249
249
  which the plugin's session-start notice offers in a repository that owns the service.
250
250
 
251
+ **Say what the agent uses** (`fabric-connections/0.1`, DEC-0096) so Fabric Dashboards draws its chain: the other
252
+ agents, models, connectors, stores and channels, each with its state, from
253
+ `connections_extension()` / `connectionsExtension()` in `fabric_connections`. Never a credential in it, never a call
254
+ count (that is the usage report); the details are in [the wire reference](references/protocol.md).
255
+
251
256
  The events feed is a **view over the log you already keep** (a jobs table, a JSONL
252
257
  journal), not a second store. Each event is one sentence a person reads; set
253
258
  `notify: true` only when the operator must decide, or something failed or degrades their
@@ -284,7 +289,7 @@ propagation, event trace pairs, and `board.responder` for a service that declare
284
289
  any `FAIL`. A `NOT_RUN` is not a pass;
285
290
  name it in the report. A service with a dashboard also runs the layout probe,
286
291
  `node <skill-dir>/scripts/check_dashboard_layout.mjs --service <id>` (WebKit at 360, 480, 768
287
- and 1280 px; needs Playwright, else `NOT_RUN`).
292
+ and 1280 px; needs Playwright, else `NOT_RUN` and exit 3: nothing was measured).
288
293
 
289
294
  Then prove the lock: start a second copy by hand against the same data directory; it
290
295
  must exit 75 while the first keeps serving. Prove recovery: `kill -9` the launchd pid;
@@ -137,7 +137,8 @@ the floor minus 40 and at 360, 480, 768 and 1280 px, and reports ED-1…ED-5 per
137
137
  Playwright where it runs (`npm i -D playwright && npx playwright install webkit`, or `--playwright
138
138
  <dir>`); without it every rule is `NOT_RUN`, and the probe asks for no login code. ED-3 also fails a link clipped
139
139
  vertically by the viewport or a clipping ancestor; ED-4 is `NOT_RUN` on a page past its 6000-element cap. It does not measure edge shades or that a refresh keeps
140
- the scroll position: say so in the report. A `NOT_RUN` is not a pass.
140
+ the scroll position: say so in the report. A `NOT_RUN` is not a pass. Exit codes: 0 when no rule fails and at least
141
+ one passed, 1 on a `FAIL`, 2 on a usage error, 3 when no rule was measured (every rule `NOT_RUN`, since 0.44.0).
141
142
 
142
143
  ## Upgrading an existing dashboard
143
144
 
@@ -63,6 +63,7 @@ A module imports only modules listed before it in `kit.json`, so the kit has no
63
63
  | L3 | `fabric_platform` | Windows: named pipes, owner-only files (DEC-0032) |
64
64
  | L3 | `fabric_paths` | the services directory, a service's folders, atomic and private writes |
65
65
  | L3 | `fabric_protocols` | protocol revisions (DEC-0067) and feed data (DEC-0070) |
66
+ | L3 | `fabric_connections` | what an agent uses, `fabric-connections/0.1`: the reference reading and the declaration (DEC-0096, FAC-SEM-127) |
66
67
  | L3 | `fabric_lock` | the instance lock and the supervisor marker (LC-01, DEC-0057) |
67
68
  | L3 | `fabric_usage` | usage receipts, the ledger, the usage report, attribution, spending limits ([usage](usage.md)) |
68
69
  | L3 | `fabric_backups` | settings backups (DEC-0025) |
@@ -1,6 +1,6 @@
1
1
  # fabric-service/0.1: wire reference
2
2
 
3
- Pinned to `fabric-agent-contract` commit `c55f2d5a11c9a0197e958e87212fca7ae1154769`
3
+ Pinned to `fabric-agent-contract` commit `bfa9a67aa3c6cb808c093e7d83cd59799f1ee7d0`
4
4
  (`docs/specification/service.md`, DEC-0015; the usage report DEC-0021; Windows and Linux supervision
5
5
  DEC-0032 and DEC-0033; the operator channel DEC-0034 and DEC-0035; the embedded dashboard DEC-0036;
6
6
  delegation and the operator session's lifetime DEC-0039; the meter's call window DEC-0041; session handoff DEC-0042;
@@ -8,7 +8,8 @@ canonical forms DEC-0043; `degraded[].setup` and `run.mcp.stdio.scope` DEC-0044;
8
8
  DEC-0045; runner and model profiles DEC-0046; the estate record DEC-0055; one copy under any supervisor and its marker,
9
9
  the Windows token-file reader DEC-0057; the hub key the estate certifies DEC-0060; signing credentials DEC-0063; the
10
10
  estate record's 60 s skew DEC-0066; an app's own descriptor DEC-0069; the descriptor for listings DEC-0061; identifier equality
11
- by normal form DEC-0076; the project board's directory page DEC-0077 and a replacement's transferred attempts DEC-0078; an operator's hold DEC-0079, a structured finding DEC-0080 and the owner's settlement DEC-0081). The contract's schemas are normative;
11
+ by normal form DEC-0076; the project board's directory page DEC-0077 and a replacement's transferred attempts DEC-0078; an operator's hold DEC-0079, a structured finding DEC-0080 and the owner's settlement DEC-0081; `fabric-connections/0.1` in the protocol register
12
+ DEC-0096; a schema pattern never uses a bare dot DEC-0097). The contract's schemas are normative;
12
13
  this page is the working summary. Extension key:
13
14
  `https://fabric.passioncode.ai/agent-contract/extensions/service/0.1`.
14
15
 
@@ -92,6 +93,26 @@ No auth; the Host/Origin guard applies; answered from memory in under 100 ms.
92
93
  ED-9): `minWidth` 360…1280 (required), `appearance` ⊆ `scheme`, `theme`, `themes`, `kit`
93
94
  (`name@semver`), nothing else. The kits build it with `dashboard_extension()` /
94
95
  `dashboardExtension()`; see [the dashboard kit reference](dashboard-kit.md).
96
+ - `extensions` MAY say what the service uses under
97
+ `https://fabric.passioncode.ai/agent-contract/extensions/connections/0.1` (`fabric-connections/0.1`, DEC-0096):
98
+ `{connections: [...]}`, at most 64, each `{id, kind, target, state}` with `kind` one of `service`, `model`,
99
+ `connector`, `store`, `channel`, `state` one of `ok`, `degraded`, `down`, `unknown`, and optionally `capability`
100
+ (a `service` only), `label`, `purpose`, `reason`, `lastAt`. No member carries a credential, and each `id` is named
101
+ once: a host hides a list that breaks either rule (FAC-SEM-127). A `degraded[].source` equal to an `id` is that
102
+ connection failing. The kits build it with `connections_extension(declared, degraded, states)` /
103
+ `connectionsExtension(declared, {degraded, states})`: `state` comes from the service's own checks (`states`, by
104
+ `id`), else `degraded` when `degraded[]` names it, else `unknown`; a declaration a host would hide is refused with
105
+ `ServiceError`, and `build_well_known` refuses one passed in by hand. `connections_view` / `connectionsView` read
106
+ a peer's list as Fabric Dashboards does, and `check_service.py` reports `well-known.connections`. Build it each
107
+ time the well-known document is built, from the same `degraded` list:
108
+
109
+ ```python
110
+ ext = fs.dashboard_extension()
111
+ ext.update(connections_extension(
112
+ [{"id": "router", "kind": "model", "target": "openai/gpt-5.1", "purpose": "drafts replies"},
113
+ {"id": "notify", "kind": "service", "target": "communicator.default", "capability": "communicator.notify"}],
114
+ degraded=degraded, states={"router": "ok" if model_answered else "down"}))
115
+ ```
95
116
  - `surfaces.mcp.auth`: `descriptor` (default), `own` (DEC-0024) or `delegation` (DEC-0039, DL-16):
96
117
  the MCP surface takes only the estate's delegation tokens, and the top-level
97
118
  `auth.delegation {issuer, jwks}` is then required. With `own` or `delegation` the descriptor's
@@ -13,7 +13,8 @@
13
13
  // scroll box) and ED-5 (sticky chrome within min(120 px, a quarter of the height), anchors landing
14
14
  // below it, no nested full-height scroller). What it cannot measure is NOT_RUN with the reason,
15
15
  // never a pass. WebKit is the default engine (Safari's). Exit 0 when nothing FAILs, 1 when
16
- // something does, 2 on a usage error or a report with no rule in it. It reads; it never clicks a control of the page.
16
+ // something does, 2 on a usage error, 3 when no rule was measured (every rule NOT_RUN). It reads; it never clicks a
17
+ // control of the page.
17
18
  import { readFileSync, existsSync, realpathSync } from 'node:fs';
18
19
  import { createRequire } from 'node:module';
19
20
  import { homedir, platform } from 'node:os';
@@ -261,11 +262,13 @@ export function report(rows, opts, target) {
261
262
  for (const r of rows) process.stdout.write(`${r.verdict.padEnd(8)} ${(r.rule + (r.width ? '@' + r.width : '')).padEnd(10)} ${r.evidence}\n`);
262
263
  process.stdout.write(`\n${rows.length} rule(s), ${failed} FAIL, ${rows.filter((r) => r.verdict === 'NOT_RUN').length} NOT_RUN\n`);
263
264
  }
264
- if (!rows.length) {
265
- process.stderr.write('no rule ran: nothing was measured, so nothing passed\n');
266
- return 2;
265
+ if (failed) return 1;
266
+ // Nothing measured is never a pass (FAA-96): no rule, or every rule NOT_RUN, exits 3.
267
+ if (!rows.some((r) => r.verdict === 'PASS')) {
268
+ process.stderr.write('no rule was measured, so nothing passed\n');
269
+ return 3;
267
270
  }
268
- return failed ? 1 : 0;
271
+ return 0;
269
272
  }
270
273
 
271
274
  async function main(argv) {
@@ -52,6 +52,7 @@ from typing import Any, Dict, List, Optional, Tuple
52
52
 
53
53
  sys.path.insert(0, str(Path(__file__).resolve().parent))
54
54
  import fabric_service as fs # noqa: E402
55
+ import fabric_connections as fcn # noqa: E402
55
56
  import fabric_interop as fi # noqa: E402
56
57
 
57
58
  Result = Dict[str, str]
@@ -238,6 +239,14 @@ class Probe:
238
239
  findings = fs.protocols_findings(wk["protocols"])
239
240
  self.add("well-known.protocols", "FAIL" if findings else "PASS",
240
241
  "; ".join("%s %s" % (f["instancePath"], f["message"]) for f in findings) or ", ".join(wk["protocols"]))
242
+ extensions = wk.get("extensions") if isinstance(wk.get("extensions"), dict) else {}
243
+ if fcn.CONNECTIONS_EXTENSION_KEY in extensions:
244
+ # DEC-0096, FAC-SEM-127: what it uses, valid, each id once, no credential; a host hides an invalid list.
245
+ view = fcn.connections_view(wk)
246
+ failing = sum(1 for c in view["connections"] if c["failing"])
247
+ self.add("well-known.connections", "FAIL" if view["reported"] else "PASS",
248
+ "; ".join(f["message"] for f in fcn.connections_rules(wk)[:5]) or
249
+ "%d connection(s), %d named in degraded[]" % (len(view["connections"]), failing))
241
250
  self.configuration_rule()
242
251
  svc = wk.get("service") or {}
243
252
  answered = "%s.%s" % (svc.get("id"), svc.get("instance"))
@@ -18,11 +18,11 @@ export const SETTINGS_BACKUP_MAX_AGE_SECONDS = 24 * 60 * 60;
18
18
 
19
19
  const TABLE = /^[a-z][a-z0-9_]{0,62}$/;
20
20
 
21
- const SECRET_TABLE = /^((.+_)?(account_connections?|account_connection_drafts?|tokens?|secrets?|credentials?|passwords?|login_codes?)|(operator_)?sessions?)$/;
21
+ const SECRET_TABLE = /^(([\s\S]+_)?(account_connections?|account_connection_drafts?|tokens?|secrets?|credentials?|passwords?|login_codes?)|(operator_)?sessions?)$/;
22
22
 
23
23
  const COLUMN = /^[A-Za-z_][A-Za-z0-9_]{0,62}$/;
24
24
 
25
- const SECRET_COLUMN = /^(?:.+_)?(?:[Tt]oken|[Ss]ecret|[Pp]assword|[Pp]assphrase|[Aa]pi_?[Kk]ey|[Pp]rivate_[Kk]ey|[Cc]ookie|[Ll]ogin_[Cc]ode|[Oo]tp|[Cc]redential)s?$/;
25
+ const SECRET_COLUMN = /^(?:[\s\S]+_)?(?:[Tt]oken|[Ss]ecret|[Pp]assword|[Pp]assphrase|[Aa]pi_?[Kk]ey|[Pp]rivate_[Kk]ey|[Cc]ookie|[Ll]ogin_[Cc]ode|[Oo]tp|[Cc]redential)s?$/;
26
26
 
27
27
  const REASON = /^[a-z][a-z0-9_.-]{0,63}$/;
28
28
 
@@ -0,0 +1,186 @@
1
+ // What an agent uses: fabric-connections/0.1, the extension keyed extensions/connections/0.1 (DEC-0096, FAC-SEM-127).
2
+ // connectionsView reads a well-known document as the contract's connectionsView does (it never throws);
3
+ // connectionsExtension builds the `extensions` entry a service declares, `state` from its own checks and its degraded[].
4
+ // Part of the Node service kit (FAA-62, layer L3); the twin of fabric_connections.py. Node 20+, no dependency.
5
+ import { ServiceError } from './fabric-base.mjs';
6
+ import { BEARER_INSIDE, JWT_SHAPE, longSecret, secretUrl } from './fabric-secrets.mjs';
7
+ import { isObject } from './fabric-jsvalue.mjs';
8
+
9
+ export const CONNECTIONS_EXTENSION_KEY = 'https://fabric.passioncode.ai/agent-contract/extensions/connections/0.1';
10
+ export const KINDS = Object.freeze(['service', 'model', 'connector', 'store', 'channel']);
11
+ export const STATES = Object.freeze(['ok', 'degraded', 'down', 'unknown']);
12
+ export const MAX_CONNECTIONS = 64;
13
+ const MEMBERS = ['id', 'kind', 'target', 'capability', 'state', 'label', 'purpose', 'reason', 'lastAt'];
14
+ const STRING_MEMBERS = ['id', 'target', 'capability', 'label', 'purpose', 'reason'];
15
+
16
+ const ID = /^[a-z0-9][a-z0-9._-]{0,63}$/u;
17
+ const TARGET = /^[^\s]+$/u;
18
+ const SERVICE_TARGET = /^[a-z0-9][a-z0-9-]{0,63}(\.[a-z0-9][a-z0-9-]{0,63})?$/u;
19
+ const CAPABILITY = /^[a-z][a-z0-9._-]{1,127}$/u;
20
+ const LINE = /^[^\n\r]+$/u;
21
+
22
+ // ajv-formats `date-time` in its full mode (the contract's validator): a date, `t` or a space, a time with a zone.
23
+ const DATE = /^(\d\d\d\d)-(\d\d)-(\d\d)$/;
24
+ const TIME = /^(\d\d):(\d\d):(\d\d(?:\.\d+)?)(z|([+-])(\d\d)(?::?(\d\d))?)?$/i;
25
+ const DAYS = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
26
+ function fullDate(value) {
27
+ const m = DATE.exec(value);
28
+ if (!m) return false;
29
+ const [year, month, day] = [+m[1], +m[2], +m[3]];
30
+ const leap = year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
31
+ return month >= 1 && month <= 12 && day >= 1 && day <= (month === 2 && leap ? 29 : DAYS[month]);
32
+ }
33
+ function fullTime(value) {
34
+ const m = TIME.exec(value);
35
+ if (!m) return false;
36
+ const [hour, minute, second] = [+m[1], +m[2], +m[3]];
37
+ const sign = m[5] === '-' ? -1 : 1;
38
+ const [zoneH, zoneM] = [+(m[6] || 0), +(m[7] || 0)];
39
+ if (zoneH > 23 || zoneM > 59 || !m[4]) return false;
40
+ if (hour <= 23 && minute <= 59 && second < 60) return true;
41
+ const utcMin = minute - zoneM * sign; // a leap second, at 23:59 UTC only
42
+ const utcHour = hour - zoneH * sign - (utcMin < 0 ? 1 : 0);
43
+ return (utcHour === 23 || utcHour === -1) && (utcMin === 59 || utcMin === -1) && second < 61;
44
+ }
45
+ /** The `date-time` format as the contract's validator (ajv-formats, full mode) reads it. */
46
+ export function dateTime(value) {
47
+ const parts = value.split(/t|\s/i);
48
+ return parts.length === 2 && fullDate(parts[0]) && fullTime(parts[1]);
49
+ }
50
+
51
+ const length = (value) => [...value].length; // code points, as ajv counts them
52
+
53
+ /** service-connections-extension.schema.json, read as the contract's validator reads it. */
54
+ function shapeProblems(value) {
55
+ if (!isObject(value)) return ['/ type: must be object'];
56
+ // A member's name is never echoed: a secret-shaped name would reach the error text and a probe's evidence.
57
+ const problems = Object.keys(value).filter((key) => key !== 'connections').map(() => '/ additionalProperties: a member other than connections');
58
+ if (!Object.hasOwn(value, 'connections')) return [...problems, '/ required: connections'];
59
+ const connections = value.connections;
60
+ if (!Array.isArray(connections)) return [...problems, '/connections type: must be array'];
61
+ if (connections.length > MAX_CONNECTIONS) problems.push(`/connections maxItems: at most ${MAX_CONNECTIONS}`);
62
+ connections.forEach((item, index) => {
63
+ const at = `/connections/${index}`;
64
+ if (!isObject(item)) { problems.push(`${at} type: must be object`); return; }
65
+ for (const key of ['id', 'kind', 'target', 'state']) if (!Object.hasOwn(item, key)) problems.push(`${at} required: ${key}`);
66
+ for (const key of Object.keys(item)) if (!MEMBERS.includes(key)) problems.push(`${at} additionalProperties: a member the schema does not name`);
67
+ for (const key of [...STRING_MEMBERS, 'lastAt']) if (Object.hasOwn(item, key) && typeof item[key] !== 'string') problems.push(`${at}/${key} type: must be string`);
68
+ const text = (key) => (Object.hasOwn(item, key) && typeof item[key] === 'string' ? item[key] : undefined);
69
+ if (text('id') !== undefined && !ID.test(text('id'))) problems.push(`${at}/id pattern`);
70
+ if (Object.hasOwn(item, 'kind') && !KINDS.includes(item.kind)) problems.push(`${at}/kind enum`);
71
+ if (Object.hasOwn(item, 'state') && !STATES.includes(item.state)) problems.push(`${at}/state enum`);
72
+ const target = text('target');
73
+ if (target !== undefined && !(length(target) >= 1 && length(target) <= 200 && TARGET.test(target))) problems.push(`${at}/target length or pattern`);
74
+ if (text('capability') !== undefined && !CAPABILITY.test(text('capability'))) problems.push(`${at}/capability pattern`);
75
+ for (const key of ['label', 'purpose', 'reason']) {
76
+ const line = text(key);
77
+ if (line !== undefined && !(length(line) >= 1 && length(line) <= 300 && LINE.test(line))) problems.push(`${at}/${key} length or pattern`);
78
+ }
79
+ if (text('lastAt') !== undefined && !dateTime(text('lastAt'))) problems.push(`${at}/lastAt format: date-time`);
80
+ if (Object.hasOwn(item, 'kind')) {
81
+ if (item.kind !== 'service' && Object.hasOwn(item, 'capability')) problems.push(`${at}/capability: only a service connection names a capability`);
82
+ if (item.kind === 'service' && target !== undefined && !SERVICE_TARGET.test(target)) problems.push(`${at}/target pattern: a service target is an id.instance key or a product id`);
83
+ }
84
+ });
85
+ return problems;
86
+ }
87
+
88
+ // The contract's KEY_SHAPE (src/connections.ts): a provider key where it starts a word, case-sensitive prefixes, the
89
+ // `sk-` rules and a Telegram bot token.
90
+ const KEY_SHAPE = /(?:^|[^A-Za-z0-9])(?:sk-(?=[A-Za-z0-9_-]*\d)(?:(?=[A-Za-z0-9_-]*[A-Z])[A-Za-z0-9_-]{16,}|[A-Za-z0-9_-]{32,})|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,}|(?:bot)?\d{6,12}:(?=[A-Za-z0-9_-]*[A-Z])(?=[A-Za-z0-9_-]*[a-z])[A-Za-z0-9_-]{34,35}(?![A-Za-z0-9_-]))/;
91
+ const SCHEME = /[a-z][a-z0-9+.-]*:\/\//gi;
92
+ const CLOSING = /[)\]}>"'.,;:!?]/;
93
+ const TRIED_BACK = 1;
94
+ /** The URLs a member may hold, from every scheme in each whitespace-delimited piece, without the closing brackets,
95
+ * quotes and punctuation that end a sentence, and with TRIED_BACK of them put back (`]` closes an IPv6 host). */
96
+ function urlsIn(value) {
97
+ const urls = [];
98
+ for (const piece of value.split(/\s+/)) {
99
+ for (const match of piece.matchAll(SCHEME)) {
100
+ const url = piece.slice(match.index);
101
+ let end = url.length;
102
+ while (end > 0 && CLOSING.test(url[end - 1])) end -= 1;
103
+ for (let back = 0; back <= TRIED_BACK && end + back <= url.length; back += 1) urls.push(url.slice(0, end + back));
104
+ }
105
+ }
106
+ return urls;
107
+ }
108
+ /** A long secret-shaped run in a piece read whole; in a piece that holds a URL, the text before its first scheme's `://`. */
109
+ const longRun = (value) =>
110
+ value.split(/\s+/).some((piece) => {
111
+ const at = piece.search(SCHEME);
112
+ const cut = at < 0 ? -1 : piece.indexOf('://', at);
113
+ return longSecret(cut < 0 ? piece : piece.slice(0, cut));
114
+ });
115
+ const credential = (value, runs) =>
116
+ JWT_SHAPE.test(value) || KEY_SHAPE.test(value) || BEARER_INSIDE.test(value) || (runs && longRun(value)) || urlsIn(value).some(secretUrl);
117
+
118
+ /** What the schema cannot state: an id named twice, a credential in any string member. */
119
+ function beyondShape(list) {
120
+ const errors = [];
121
+ const seen = new Set();
122
+ list.forEach((connection, index) => {
123
+ if (seen.has(connection.id)) errors.push(`/connections/${index}/id duplicate: it is named twice`);
124
+ seen.add(connection.id);
125
+ for (const member of STRING_MEMBERS) {
126
+ const value = connection[member];
127
+ const runs = member !== 'id' && member !== 'capability' && !(member === 'target' && connection.kind === 'model');
128
+ if (typeof value === 'string' && credential(value, runs)) errors.push(`/connections/${index}/${member} credential: a connection never carries a credential`);
129
+ }
130
+ });
131
+ return errors;
132
+ }
133
+
134
+ const none = () => ({ declared: false, reported: false, connections: [], errors: [] });
135
+
136
+ /** A host's reading of a well-known document's connections (the contract's connectionsView): never throws. */
137
+ export function connectionsView(wellKnown) {
138
+ const extensions = isObject(wellKnown) && isObject(wellKnown.extensions) ? wellKnown.extensions : undefined;
139
+ if (extensions === undefined || !Object.hasOwn(extensions, CONNECTIONS_EXTENSION_KEY)) return none();
140
+ const value = extensions[CONNECTIONS_EXTENSION_KEY];
141
+ let errors = shapeProblems(value);
142
+ if (!errors.length) errors = beyondShape(value.connections);
143
+ if (errors.length) return { ...none(), reported: true, errors };
144
+ const degraded = Array.isArray(wellKnown.degraded) ? wellKnown.degraded : [];
145
+ const failing = new Set(degraded.filter(isObject).map((entry) => entry.source).filter((source) => typeof source === 'string'));
146
+ return { declared: true, reported: false, connections: value.connections.map((c) => ({ ...c, failing: failing.has(c.id) })), errors: [] };
147
+ }
148
+
149
+ const pointerToken = (token) => token.replace(/~/g, '~0').replace(/\//g, '~1');
150
+
151
+ /** FAC-SEM-127: the connections a well-known document declares are valid, each id once, no credential. */
152
+ export function connectionsRules(wellKnown) {
153
+ const view = connectionsView(wellKnown);
154
+ const at = `/extensions/${pointerToken(CONNECTIONS_EXTENSION_KEY)}`;
155
+ return view.errors.map((error) => ({ code: 'FAC-SEM-127', instancePath: at, message: `the connections declaration is invalid (${error}); a host treats it as not declared and reports it (DEC-0096)` }));
156
+ }
157
+
158
+ /**
159
+ * The `extensions` entry a service declares (DEC-0096): what it uses, each connection's `state` from the service's
160
+ * own checks (`states`, by id), else `degraded` when a degraded[].source names it, else `unknown`; a declared `state`
161
+ * is kept. Throws ServiceError on a declaration a host would hide: a service never publishes one that reads as
162
+ * "declared nothing".
163
+ */
164
+ export function connectionsExtension(declared, options = {}) {
165
+ const { degraded: givenDegraded, states: givenStates } = options ?? {};
166
+ const degraded = givenDegraded ?? [];
167
+ const states = givenStates ?? {};
168
+ if (!Array.isArray(declared) || !Array.isArray(degraded) || !isObject(states)) {
169
+ throw new ServiceError('Connections declaration: declared and degraded are lists, states an object by id.');
170
+ }
171
+ const failing = new Set(degraded.filter(isObject).map((entry) => entry.source).filter((source) => typeof source === 'string'));
172
+ const connections = declared.map((item) => {
173
+ if (!isObject(item)) return item; // the shape check names it and the declaration is refused
174
+ const connection = { ...item };
175
+ if (!Object.hasOwn(connection, 'state')) {
176
+ const id = typeof connection.id === 'string' ? connection.id : undefined;
177
+ // An invalid check value is refused by the shape check, never read as a missing one.
178
+ connection.state = id !== undefined && Object.hasOwn(states, id) ? states[id] : (failing.has(id) ? 'degraded' : 'unknown');
179
+ }
180
+ return connection;
181
+ });
182
+ const value = { connections };
183
+ const view = connectionsView({ extensions: { [CONNECTIONS_EXTENSION_KEY]: value } });
184
+ if (view.reported) throw new ServiceError(`Connections declaration: ${view.errors.join('; ')}.`);
185
+ return { [CONNECTIONS_EXTENSION_KEY]: value };
186
+ }
@@ -188,7 +188,8 @@ export function chainDepth(act) {
188
188
 
189
189
  function chainOf(act) {
190
190
  const chain = [];
191
- for (let node = act; node; node = 'act' in node ? node.act : undefined) chain.push('partner' in node ? node.partner : node.sub);
191
+ // The contract's chainOf at bfa9a67: a node names its partner only when it is a partner actor (the partner member alone).
192
+ for (let node = act; node; node = 'act' in node ? node.act : undefined) chain.push(isPartnerActor(node) ? node.partner : node.sub);
192
193
  return chain;
193
194
  }
194
195
 
@@ -6,6 +6,7 @@ import { credentialName, secretArgument, secretUrl, secretValue } from './fabric
6
6
  import { ID, INSTANCE, LOCAL_PATH, PROTOCOL, ServiceError, TASK, UNIT, expand } from './fabric-base.mjs';
7
7
  import { atomicWrite, ensurePrivateDir, servicesDir } from './fabric-paths.mjs';
8
8
  import { protocolsFindings } from './fabric-protocols.mjs';
9
+ import { CONNECTIONS_EXTENSION_KEY, connectionsView } from './fabric-connections.mjs';
9
10
  import { ROUTES } from './fabric-usage.mjs';
10
11
 
11
12
  // The embedded dashboard declaration (fabric-dashboard/0.1, DEC-0036 ED-9), under the well-known
@@ -369,6 +370,11 @@ export function buildWellKnown({ id, instance, name, version, build, startedAt,
369
370
  const problems = dashboardExtensionProblems(extensions[DASHBOARD_EXTENSION_KEY]);
370
371
  if (problems.length) throw new ServiceError(`Dashboard declaration: ${problems.join('; ')}.`);
371
372
  }
373
+ if (Object.hasOwn(extensions, CONNECTIONS_EXTENSION_KEY)) {
374
+ // DEC-0096: a host hides an invalid declaration (FAC-SEM-127); a service never publishes one.
375
+ const view = connectionsView({ extensions: { [CONNECTIONS_EXTENSION_KEY]: extensions[CONNECTIONS_EXTENSION_KEY] } });
376
+ if (view.reported) throw new ServiceError(`Connections declaration: ${view.errors.join('; ')}.`);
377
+ }
372
378
  doc.extensions = { ...extensions };
373
379
  }
374
380
  if (configuration !== undefined) {
@@ -557,7 +557,7 @@
557
557
  },
558
558
  "path": {
559
559
  "type": "string",
560
- "pattern": "^(?![/~])(?!.*\\\\)(?!.*//)(?!(?:.*/)?\\.{1,2}(?:/|$))[^\\u0000-\\u001f:]{1,256}(?<!/)$"
560
+ "pattern": "^(?![/~])(?![\\s\\S]*\\\\)(?![\\s\\S]*//)(?!(?:[\\s\\S]*/)?\\.{1,2}(?:/|$))[^\\u0000-\\u001f:]{1,256}(?<!/)$"
561
561
  },
562
562
  "common.$defs.semver": {
563
563
  "type": "string",
@@ -8,7 +8,7 @@ import { credentialName, secretArgument, secretUrl, secretValue } from './fabric
8
8
  /** Each contract protocol's published revisions, oldest first (the contract's `PROTOCOL_REVISIONS`); a test holds it equal to the contract's. */
9
9
  export const PROTOCOL_REVISIONS = new Map(Object.entries({
10
10
  'access-log': ['1'], approval: ['1'], 'activity-summary': ['1'], 'coding-agents': ['1'], 'enroll-token': ['1'], entitlement: ['1'], 'entitlement-key-set': ['1'], 'estate-record': ['1'], 'execution-packet': ['1'],
11
- 'fabric-activity': ['0.1'], 'fabric-dashboard': ['0.1'], 'fabric-delegation': ['0.1'], 'fabric-device': ['0.1'],
11
+ 'fabric-activity': ['0.1'], 'fabric-connections': ['0.1'], 'fabric-dashboard': ['0.1'], 'fabric-delegation': ['0.1'], 'fabric-device': ['0.1'],
12
12
  'fabric-interop': ['0.1', '0.2'], 'fabric-local-channel': ['0.1'], 'fabric-local-runner': ['0.1'], 'fabric-operator-channel': ['0.1'],
13
13
  'fabric-package': ['1'], 'fabric-project-comms': ['0.1'], 'fabric-proposal': ['1'], 'fabric-provider': ['0.1'], 'fabric-relay': ['0.1'],
14
14
  'fabric-secret-envelope': ['1'], 'fabric-service': ['0.1'], 'fabric-settings-backup': ['1'], heartbeat: ['1'], 'learning-note': ['1'],
@@ -41,12 +41,12 @@ export const JWT_SHAPE = /eyJ[A-Za-z0-9_-]{2,}\.[A-Za-z0-9_-]{2,}\.[A-Za-z0-9_-]
41
41
  /** A value that says it is a credential, or a provider key's shape. */
42
42
  export const 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;
43
43
  const LONG_RUN = /[A-Za-z0-9+/=_-]{40,}/g;
44
- const longSecret = (value) => (value.match(LONG_RUN) ?? []).some((run) => /[a-z]/.test(run) && /[A-Z]/.test(run) && /[0-9]/.test(run));
44
+ export const longSecret = (value) => (value.match(LONG_RUN) ?? []).some((run) => /[a-z]/.test(run) && /[A-Z]/.test(run) && /[0-9]/.test(run));
45
45
 
46
46
  /** Whether a value could be a credential: a JWT anywhere, a provider key or `Bearer …`, or a long secret-shaped run. */
47
47
  export const secretValue = (value) => JWT_SHAPE.test(value) || SECRET_VALUE.test(value) || longSecret(value);
48
48
 
49
- const BEARER_INSIDE = /\bbearer\s+(?=[A-Za-z0-9._~+/-]*\d)[A-Za-z0-9._~+/-]{12,}=*/i;
49
+ export const BEARER_INSIDE = /\bbearer\s+(?=[A-Za-z0-9._~+/-]*\d)[A-Za-z0-9._~+/-]{12,}=*/i;
50
50
  const PATH_LIKE = /^(?:[/~.]|[A-Za-z]:[\\/]|[a-z][a-z0-9+.-]*:\/\/)/i;
51
51
  const pathLike = (piece) =>
52
52
  PATH_LIKE.test(piece) ||
@@ -40,6 +40,7 @@ export {
40
40
  feedDataFindings,
41
41
  feedDataProblems,
42
42
  } from './fabric-protocols.mjs';
43
+ export { CONNECTIONS_EXTENSION_KEY, connectionsExtension, connectionsRules, connectionsView } from './fabric-connections.mjs';
43
44
  export {
44
45
  EXIT_ALREADY_RUNNING,
45
46
  LOCK_WAIT_SUPERVISED_SECONDS,
@@ -49,13 +49,13 @@ _SAFE_INTEGER = 2 ** 53 - 1
49
49
  _TABLE = re.compile(r"^[a-z][a-z0-9_]{0,62}\Z")
50
50
 
51
51
 
52
- _SECRET_TABLE = re.compile(r"^((.+_)?(account_connections?|account_connection_drafts?|tokens?|secrets?|credentials?|passwords?|login_codes?)|(operator_)?sessions?)\Z")
52
+ _SECRET_TABLE = re.compile(r"^(([\s\S]+_)?(account_connections?|account_connection_drafts?|tokens?|secrets?|credentials?|passwords?|login_codes?)|(operator_)?sessions?)\Z")
53
53
 
54
54
 
55
55
  _COLUMN = re.compile(r"^[A-Za-z_][A-Za-z0-9_]{0,62}\Z")
56
56
 
57
57
 
58
- _SECRET_COLUMN = re.compile(r"^(?:.+_)?(?:[Tt]oken|[Ss]ecret|[Pp]assword|[Pp]assphrase|[Aa]pi_?[Kk]ey|[Pp]rivate_[Kk]ey|[Cc]ookie|[Ll]ogin_[Cc]ode|[Oo]tp|[Cc]redential)s?\Z")
58
+ _SECRET_COLUMN = re.compile(r"^(?:[\s\S]+_)?(?:[Tt]oken|[Ss]ecret|[Pp]assword|[Pp]assphrase|[Aa]pi_?[Kk]ey|[Pp]rivate_[Kk]ey|[Cc]ookie|[Ll]ogin_[Cc]ode|[Oo]tp|[Cc]redential)s?\Z")
59
59
 
60
60
 
61
61
  _REASON = re.compile(r"^[a-z][a-z0-9_.-]{0,63}\Z")