@passioncode-ai/passioncode 0.2.22 → 0.2.24
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 +16 -0
- package/family.json +1 -1
- package/package.json +1 -1
- package/payload/.claude-plugin/marketplace.json +2 -2
- package/payload/manifest.json +8 -8
- package/payload/plugins/fabric-agent-adapter/.claude-plugin/plugin.json +1 -1
- package/payload/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/SKILL.md +1 -1
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +7 -2
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard-kit.md +2 -1
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/kit-modules.md +7 -3
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/protocol.md +20 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_dashboard_layout.mjs +8 -5
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service.py +9 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-connections.mjs +186 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-descriptor.mjs +6 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-interop-rules.mjs +264 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-interop.mjs +31 -254
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-package-schema.mjs +100 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-package.mjs +2 -89
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-secrets.mjs +2 -2
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-service.mjs +1 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_connections.py +448 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_descriptor.py +6 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_interop.py +54 -369
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_interop_rules.py +390 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_package.py +20 -187
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_package_schema.py +207 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_service.py +6 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/kit.json +234 -19
- package/payload/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +1 -1
- package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/SKILL.md +1 -1
- package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_env.py +1 -1
- package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_mcp.py +4 -3
- package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/host_hooks.py +3 -1
- package/payload/plugins/passioncode/.claude-plugin/plugin.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.24 - 2026-10-22
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
|
|
7
|
+
- **Fabric Agent Adapter is pinned at `v0.44.1`** (it was `v0.44.0`): two kit modules split by responsibility
|
|
8
|
+
(`fabric_interop_rules`, `fabric_package_schema`, both twins); no behaviour changed. Notes are in the adapter's
|
|
9
|
+
CHANGELOG.
|
|
10
|
+
|
|
11
|
+
## 0.2.23 - 2026-10-22
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
- **Fabric Agent Adapter is pinned at `v0.44.0`** (it was `v0.43.0`): a service built with the kit can say what it
|
|
16
|
+
uses (`fabric-connections/0.1`, the Connections tab of Fabric Dashboards), and the kit's layout check exits 3 when it
|
|
17
|
+
measured nothing. Notes are in the adapter's CHANGELOG.
|
|
18
|
+
|
|
3
19
|
## 0.2.22 - 2026-10-22
|
|
4
20
|
|
|
5
21
|
### Changed
|
package/family.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@passioncode-ai/passioncode",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.24",
|
|
4
4
|
"description": "Set up PassionCode.ai on a computer with one command — the apps from their verified releases, their MCP servers for every coding agent and the PassionCode.ai skills — and check and repair it with the same command or `doctor`.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"passioncode": "bin/passioncode.js"
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"name": "fabric-agent-adapter",
|
|
12
12
|
"displayName": "Fabric Agent Adapter",
|
|
13
13
|
"source": "./plugins/fabric-agent-adapter",
|
|
14
|
-
"version": "0.
|
|
14
|
+
"version": "0.44.1",
|
|
15
15
|
"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.",
|
|
16
16
|
"author": {
|
|
17
17
|
"name": "PassionCode.ai",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"name": "passioncode",
|
|
36
36
|
"displayName": "PassionCode.ai",
|
|
37
37
|
"source": "./plugins/passioncode",
|
|
38
|
-
"version": "0.2.
|
|
38
|
+
"version": "0.2.24",
|
|
39
39
|
"description": "Set up and check PassionCode.ai on a computer with one command (setting-up-passioncode), the organisation's working rules for every contributor's agent (working-in-passioncode), and a once-a-day check that keeps the PassionCode.ai set current.",
|
|
40
40
|
"author": {
|
|
41
41
|
"name": "PassionCode.ai",
|
package/payload/manifest.json
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"family": "passioncode",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.24",
|
|
4
4
|
"release": true,
|
|
5
5
|
"members": [
|
|
6
6
|
{
|
|
7
7
|
"name": "fabric-agent-adapter",
|
|
8
8
|
"displayName": "Fabric Agent Adapter",
|
|
9
9
|
"repo": "passioncode-ai/fabric-agent-adapter",
|
|
10
|
-
"ref": "v0.
|
|
11
|
-
"commit": "
|
|
12
|
-
"version": "0.
|
|
10
|
+
"ref": "v0.44.1",
|
|
11
|
+
"commit": "7098a93f30e6ceea8798851737e4258da4ab3eaf",
|
|
12
|
+
"version": "0.44.1",
|
|
13
13
|
"via": "clone of https://***@github.com/passioncode-ai/fabric-agent-adapter.git",
|
|
14
14
|
"skills": [
|
|
15
15
|
"adapting-projects-to-fabric",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"legacyMarketplaces": [
|
|
24
24
|
"fabric-agent-adapter"
|
|
25
25
|
],
|
|
26
|
-
"contentHash": "sha256:
|
|
26
|
+
"contentHash": "sha256:a563bc58defa9d46e6e43365473e9cbbb7af54b7b08d2c1b9d91d27e8f937eac",
|
|
27
27
|
"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.",
|
|
28
28
|
"author": {
|
|
29
29
|
"name": "PassionCode.ai",
|
|
@@ -62,9 +62,9 @@
|
|
|
62
62
|
"name": "passioncode",
|
|
63
63
|
"displayName": "PassionCode.ai",
|
|
64
64
|
"repo": "passioncode-ai/passioncode",
|
|
65
|
-
"ref": "v0.2.
|
|
65
|
+
"ref": "v0.2.24",
|
|
66
66
|
"commit": null,
|
|
67
|
-
"version": "0.2.
|
|
67
|
+
"version": "0.2.24",
|
|
68
68
|
"via": "this repository",
|
|
69
69
|
"skills": [
|
|
70
70
|
"setting-up-passioncode",
|
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
],
|
|
73
73
|
"legacyPluginIds": [],
|
|
74
74
|
"legacyMarketplaces": [],
|
|
75
|
-
"contentHash": "sha256:
|
|
75
|
+
"contentHash": "sha256:c606146b8f4661cd0ec2aeb565e7ef40961c1dcf46776c37603cb9f4d221c725",
|
|
76
76
|
"description": "Set up and check PassionCode.ai on a computer with one command (setting-up-passioncode), the organisation's working rules for every contributor's agent (working-in-passioncode), and a once-a-day check that keeps the PassionCode.ai set current.",
|
|
77
77
|
"author": {
|
|
78
78
|
"name": "PassionCode.ai",
|
|
@@ -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.
|
|
6
|
+
"version": "0.44.1",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "PassionCode.ai",
|
|
9
9
|
"url": "https://passioncode.ai/"
|
|
@@ -5,7 +5,7 @@ license: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
|
|
|
5
5
|
compatibility: Requires filesystem access and Python 3.9+. Exact schema checks additionally need git, Node.js, pnpm, and the pinned fabric-agent-contract checkout (a public repository). Works without those tools in an explicitly degraded structural-check mode.
|
|
6
6
|
metadata:
|
|
7
7
|
author: PassionCode.ai
|
|
8
|
-
version: "0.
|
|
8
|
+
version: "0.44.1"
|
|
9
9
|
contract-version: "0.1.0"
|
|
10
10
|
contract-commit: "bfa9a67aa3c6cb808c093e7d83cd59799f1ee7d0"
|
|
11
11
|
---
|
|
@@ -14,7 +14,7 @@ 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.
|
|
17
|
+
version: "0.44.1"
|
|
18
18
|
contract-version: "0.1.0"
|
|
19
19
|
extension: "fabric-service/0.1"
|
|
20
20
|
extension-commit: "bfa9a67aa3c6cb808c093e7d83cd59799f1ee7d0"
|
|
@@ -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
|
|
|
@@ -58,11 +58,13 @@ A module imports only modules listed before it in `kit.json`, so the kit has no
|
|
|
58
58
|
| L2 | `fabric_delegation` | the callee's side of `fabric-delegation/0.1` (DEC-0039) |
|
|
59
59
|
| L2 | `fabric_share` | share grants and registrations (DEC-0086) |
|
|
60
60
|
| L2 | `fabric_ceremonies` | the headless join token (DEC-0084) |
|
|
61
|
-
| L2 | `
|
|
61
|
+
| L2 | `fabric_package_schema` | the reading of `fabric-package.schema.json` with JavaScript's pattern and format semantics (FAA-92) |
|
|
62
|
+
| L2 | `fabric_package` | `fabric-package/1`: pack, sign, verify; re-exports `schema_problems` (DEC-0050, DEC-0051) |
|
|
62
63
|
| L3 | `fabric_base` | the protocol name, `ServiceError`, small shared helpers |
|
|
63
64
|
| L3 | `fabric_platform` | Windows: named pipes, owner-only files (DEC-0032) |
|
|
64
65
|
| L3 | `fabric_paths` | the services directory, a service's folders, atomic and private writes |
|
|
65
66
|
| L3 | `fabric_protocols` | protocol revisions (DEC-0067) and feed data (DEC-0070) |
|
|
67
|
+
| L3 | `fabric_connections` | what an agent uses, `fabric-connections/0.1`: the reference reading and the declaration (DEC-0096, FAC-SEM-127) |
|
|
66
68
|
| L3 | `fabric_lock` | the instance lock and the supervisor marker (LC-01, DEC-0057) |
|
|
67
69
|
| L3 | `fabric_usage` | usage receipts, the ledger, the usage report, attribution, spending limits ([usage](usage.md)) |
|
|
68
70
|
| L3 | `fabric_backups` | settings backups (DEC-0025) |
|
|
@@ -73,7 +75,8 @@ A module imports only modules listed before it in `kit.json`, so the kit has no
|
|
|
73
75
|
| L3 | `fabric_supervisors` | launchd, systemd user units, Task Scheduler (DEC-0032, LC-14) |
|
|
74
76
|
| L3 | `fabric_service` | the facade: every name 0.39.1 exported, re-exported from the modules above |
|
|
75
77
|
| L3 | `fabric_profiles` | runner and model profiles ([profiles](profiles.md)) |
|
|
76
|
-
| L3 | `
|
|
78
|
+
| L3 | `fabric_interop_rules` | `fabric-interop/0.1` rules without a service: constants, `InteropError`, trace context, job limits (DEC-0087; FAA-92) |
|
|
79
|
+
| L3 | `fabric_interop` | `fabric-interop/0.1`: jobs, envelopes, MCP tools; re-exports the rule layer ([interop](interop.md)) |
|
|
77
80
|
| L4 | `fabric_hub` | the Fabric hub's door (`fabric-hub/0.1`) |
|
|
78
81
|
| L4 | `fabric_board_shapes` | the project board's wire: constants, refusal codes, shapes |
|
|
79
82
|
| L4 | `fabric_board_link` | the board's link: the MCP session over the hub, the fsynced checkpoint, a claimed request as data |
|
|
@@ -93,7 +96,8 @@ the kit live in `../recipes/` ([the support agent](../recipes/support-agent/READ
|
|
|
93
96
|
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
97
|
name fails `test_kit_api`), and the Python facade the private names other code reached through it (`_number`,
|
|
95
98
|
`_DAY_MS`, `_utc_date_ms` from the core). `fabric_board` and `fabric-board.mjs` are
|
|
96
|
-
facades the same way
|
|
99
|
+
facades the same way, and since 0.44.1 so are `fabric_interop` (over `fabric_interop_rules`) and `fabric_package` (over
|
|
100
|
+
`fabric_package_schema`), with their Node twins: patch the rule or schema module, not the facade. What changed for a service that copies the kit: `fabric_service.py` alone is no longer enough;
|
|
97
101
|
install with `fabric_kit.py install` and `status` names what is missing. A test or service that replaced a function
|
|
98
102
|
by assigning it on the facade (`fabric_service.X = ...`) no longer changes what the kit does: the code reads its own
|
|
99
103
|
module's name, so patch the module that defines it (`fabric_lock.X`, `fabric_session_env.X`). A service that imported a module-private
|
package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/protocol.md
CHANGED
|
@@ -93,6 +93,26 @@ No auth; the Host/Origin guard applies; answered from memory in under 100 ms.
|
|
|
93
93
|
ED-9): `minWidth` 360…1280 (required), `appearance` ⊆ `scheme`, `theme`, `themes`, `kit`
|
|
94
94
|
(`name@semver`), nothing else. The kits build it with `dashboard_extension()` /
|
|
95
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
|
+
```
|
|
96
116
|
- `surfaces.mcp.auth`: `descriptor` (default), `own` (DEC-0024) or `delegation` (DEC-0039, DL-16):
|
|
97
117
|
the MCP surface takes only the estate's delegation tokens, and the top-level
|
|
98
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
|
|
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 (
|
|
265
|
-
|
|
266
|
-
|
|
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
|
|
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"))
|
|
@@ -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
|
+
}
|
|
@@ -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) {
|