@passioncode-ai/fabric-agent-adapter 0.82.0 → 0.83.1

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 (114) hide show
  1. package/CHANGELOG.md +122 -3
  2. package/README.md +6 -3
  3. package/SECURITY.md +4 -3
  4. package/package.json +1 -1
  5. package/plugins/fabric-agent-adapter/.claude-plugin/plugin.json +1 -1
  6. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/SKILL.md +7 -2
  7. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/agents-database.md +10 -0
  8. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/provider-entry.md +4 -2
  9. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/source-and-instance.md +18 -4
  10. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/adapt_project.py +16 -0
  11. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_provider.py +31 -15
  12. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_secrets.py +37 -12
  13. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_source_check.py +21 -148
  14. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_source_check_cli.py +164 -0
  15. package/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +15 -8
  16. package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-button.js +3 -3
  17. package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-dashboard.css +1 -1
  18. package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-dashboard.js +2 -2
  19. package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/kit.json +9 -4
  20. package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/data-agent/README.md +76 -0
  21. package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/data-agent/config.example.json +28 -0
  22. package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/data-agent/data_agent.py +366 -0
  23. package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/README.md +32 -17
  24. package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/config.example.json +4 -0
  25. package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/support-agent.mjs +115 -0
  26. package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/support_agent.py +63 -11
  27. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/a2a.md +98 -0
  28. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/agents-database.md +10 -0
  29. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/board.md +20 -9
  30. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/connectors.md +2 -2
  31. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/interop.md +14 -2
  32. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/kit-modules.md +5 -1
  33. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/lifecycle.md +8 -5
  34. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/ops-digest.md +75 -0
  35. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/package.md +7 -5
  36. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/protocol.md +25 -12
  37. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/remote-placement.md +10 -1
  38. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/surfaces-and-auth.md +70 -0
  39. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service_a2a.py +158 -0
  40. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-a2a-server.mjs +274 -0
  41. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-a2a.mjs +1 -1
  42. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-auth.mjs +4 -2
  43. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-fixture.mjs +14 -2
  44. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-link.mjs +54 -11
  45. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-session.mjs +45 -31
  46. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-slots.mjs +16 -6
  47. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-connections.mjs +26 -5
  48. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-delegation.mjs +3 -2
  49. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-descriptor.mjs +18 -12
  50. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-events.mjs +6 -1
  51. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-holds.mjs +21 -7
  52. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-http.mjs +3 -5
  53. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-hub.mjs +6 -2
  54. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-interop-rules.mjs +20 -1
  55. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-interop.mjs +5 -0
  56. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-lock.mjs +15 -2
  57. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-mandate.mjs +5 -0
  58. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-openrouter.mjs +24 -3
  59. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-ops-digest.mjs +455 -0
  60. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-package.mjs +17 -2
  61. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-paths.mjs +18 -5
  62. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-peer-owner.mjs +55 -19
  63. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-secrets.mjs +15 -2
  64. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-socket-proof.mjs +180 -0
  65. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-supervisors.mjs +5 -12
  66. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-tailnet-identity.mjs +468 -0
  67. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_a2a.py +2 -1
  68. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_a2a_server.py +404 -0
  69. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_auth.py +6 -2
  70. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_fixture.py +16 -3
  71. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_link.py +97 -9
  72. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_session.py +35 -48
  73. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_slots.py +15 -7
  74. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_connections.py +27 -6
  75. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_delegation.py +18 -13
  76. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_descriptor.py +23 -14
  77. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_events.py +3 -1
  78. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_holds.py +31 -7
  79. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_http.py +18 -16
  80. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_hub.py +9 -5
  81. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_interop.py +55 -96
  82. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_interop_rules.py +98 -1
  83. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_lock.py +15 -6
  84. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_openrouter.py +43 -1
  85. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_ops_digest.py +647 -0
  86. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_package.py +16 -4
  87. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_paths.py +20 -5
  88. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_peer_owner.py +93 -93
  89. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_secrets.py +37 -12
  90. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_socket_proof.py +390 -0
  91. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_supervisors.py +3 -8
  92. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_tailnet_identity.py +492 -0
  93. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/kit.json +365 -53
  94. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/sample-remote-service.mjs +2 -1
  95. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/sample_service.py +16 -7
  96. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/tailnet-groups.schema.json +69 -0
  97. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +5 -4
  98. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/agent-templates.md +16 -0
  99. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/agents-database.md +10 -0
  100. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/agents-that-answer-people.md +14 -9
  101. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/models-and-providers.md +4 -2
  102. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/source-and-instance.md +18 -4
  103. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/where-agents-run.md +2 -1
  104. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/scripts/admit.py +162 -0
  105. package/plugins/fabric-agent-adapter/skills/observing-products/SKILL.md +1 -1
  106. package/plugins/fabric-agent-adapter/skills/working-with-fabric/SKILL.md +1 -1
  107. package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/agents-database.md +10 -0
  108. package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/on-demand-session.md +14 -9
  109. package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/project-board.md +7 -4
  110. package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/session-hooks.md +10 -5
  111. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session.py +2 -0
  112. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_cli.py +16 -5
  113. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_env.py +79 -8
  114. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_paths.py +147 -45
package/CHANGELOG.md CHANGED
@@ -1,5 +1,124 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.83.1 (2026-11-13)
4
+
5
+ 0.83.0 was tagged and not published: its release run failed one Windows test of the tailnet identity module.
6
+
7
+ ### Fixed
8
+
9
+ - **A same-size rewrite of the groups file within one timestamp tick is seen** (FAA-324): `TailnetIdentity` read the
10
+ file again only when its inode, size, mtime or ctime changed, and Windows' runner left all of them as they were for
11
+ an in-place rewrite of the same size. While the file's mtime is under two seconds old, an unchanged stat now reads
12
+ its bytes and compares their SHA-256 (git's racy-clean rule), in both twins; an older unchanged stat is not read.
13
+
14
+ ## 0.83.0 (2026-11-13)
15
+
16
+ Wave 5 of the backlog sweep of 2026-11-12. The contract pin stays at `93e7db16` (`v0.1.0+2026-11-08.1`).
17
+
18
+ ### Added
19
+
20
+ - **The accepted-socket proof** (FAA-292, FAA-272): a new L0 kit module, `fabric_socket_proof` /
21
+ `fabric-socket-proof.mjs`, finds the server end of a connection, or a listener on exactly 127.0.0.1, among this OS
22
+ account's own processes (DEC-0155 RT-2; Python reads libproc, calibrated on a pair of its own, with an lsof fallback;
23
+ Node reads `lsof -F pun`). `fabric_peer_owner` asks it on a restricted macOS table before the RT-3 vouch, and stands
24
+ alone without it. `PEER_OWNER_VERSION` is `1.3.1+kit.7`.
25
+ - **Operations digests** (FAA-206 part 2): `fabric_ops_digest` and `fabric-ops-digest.mjs` fold a service's own counts
26
+ into `ops.digest.hourly@1` and `ops.digest.daily@1` (DEC-0153), held to the contract's reference folds and its
27
+ fixtures (FAC-SEM-166, FAC-SEM-167); they keep the hour the service stops in for the next run, never fill hours
28
+ across a sleep, and catch up missed days. `ops_digest_extension` writes the descriptor's OD-9 declaration. New
29
+ `references/ops-digest.md`.
30
+ - **A2A serving and calling** (FAA-10): `fabric_a2a_server` and `fabric-a2a-server.mjs` build the Agent Card from the
31
+ service's capabilities, check it before serving it, and map the A2A 1.0 JSON-RPC binding (`SendMessage`,
32
+ `SendStreamingMessage` as server-sent events, `GetTask`, `CancelTask`) onto the interop job store. `A2AClient` reaches
33
+ only origins on the operator's list, with its key from the environment. `check_service_a2a.py` holds the probe rows.
34
+ New `references/a2a.md`. The interop test against Hermes waits on a reachable endpoint.
35
+ - **A data-agent recipe** (FAA-66): `recipes/data-agent/`, numbers computed by code from a read-only source and answered
36
+ with a truth envelope where missing is never zero (built from the agents database's `data-room` template).
37
+ - **The support recipe** (FAA-116, FAA-182, FAA-264): a person's categories are routed first, a same-vendor fallback is
38
+ refused at load, the outbox is redacted, knowledge ids must be citable, and `support-agent.mjs` is `check_reply`'s
39
+ Node twin (31 shared cases).
40
+ - **`FABRIC_DATA_HOME`** puts a service's data folder on a durable volume, both `fabric_paths` twins (FAA-192).
41
+ - Guidance: the data-home base and a second context's home, the uptime A and B rows, model classes on the spend policy
42
+ or grant, a template takes precedence over the communication prompt (FAA-277), adapting step 4c scoped to an
43
+ existing agent; building `SKILL.md` names the board responder's mandate opt-in, the LaunchDaemon refusing `purge`, and
44
+ Apple's Python on macOS 27.2.
45
+ - **`admit.py`** (FAA-271): `creating-fabric-agents/scripts/admit.py` chains the source check, the probe, the strict
46
+ template check and the private-terms list into one admission check.
47
+ - **`test/validate.py --private-terms`** (FAA-243): tracked files are checked against the operator's local list of
48
+ private terms (`$PASSIONCODE_PRIVATE_TERMS`, else `~/.config/passioncode/private-terms.txt`), printing paths and lines
49
+ only; it exits 2 when the list is absent, and the release checklist runs it before a push.
50
+ - **A person's identity at every door behind `tailscale serve`** (FAA-324, board request `087f41ef`): new L0 kit module
51
+ `fabric_tailnet_identity` / `fabric-tailnet-identity.mjs`. Each door (dashboard, MCP, the service's own door) reads
52
+ `Tailscale-User-Login`, believed only when the service listens on loopback and the peer is loopback (the local
53
+ `tailscale serve`, which strips and sets the identity headers); a request without a login, a tagged node's, a Funnel
54
+ request's or one from another peer is refused with a stable code, HTTP 403 and a sentence, never a silent pass.
55
+ Groups come from a `tailnet-groups/1` file (its JSON Schema beside the module) or from the agents base's
56
+ `door-groups/1`, read for one service; the file is read again on change without a restart, and every door is closed
57
+ while it is unreadable or invalid. `references/surfaces-and-auth.md` shows the wiring.
58
+ - **`identify_agent` / `identifyAgent`** (FAA-135): a per-agent credential's agent id is bound into the mandate chain
59
+ (`McpToolServer`, `identifiedAgent`) and through the board responder.
60
+
61
+ ### Changed
62
+
63
+ - `pid_listens` takes an `owner` (this account by default, a uid, or None), and the LaunchDaemon check reads that one
64
+ lsof reader (FAA-286); a vouch is taken only for a connection that reached 127.0.0.1 (FAA-273).
65
+ - One loopback host rule (`loopback_host` / `loopbackHost`) for `fabric_http` and `fabric_delegation`; Python's
66
+ `check_url` and `JwksCache` refuse a host `new URL` refuses and one Python's IDNA codec cannot write, up front. One
67
+ base URL rule for both OpenRouter clients: a tab or line break, a non-ASCII path, a backslash or a dot segment is a
68
+ `config` error in both (Node used to rewrite and send it), and attribution reads the host as dialed (FAA-187).
69
+ - Hold records written before 0.83.0 are matched under both whitespace squashes; new records carry
70
+ `squash: "ecmascript"` (FAA-320). The connections reader finds URL schemes in linear time (FAA-322). A delegation key
71
+ set holding NaN or Infinity is refused (FAA-194). The adapter scaffold names `__pycache__/` and `*.pyc` in
72
+ `.gitignore` (FAA-190).
73
+ - `AGENTS.md` carries item 1a, the course check before every change (fabric-workspace `knowledge/rules.md` §0).
74
+ - Private terms in `CHANGELOG.md` and `docs/backlog.md` are replaced by neutral words; ids and findings stay (FAA-243).
75
+ - `fabric_secrets.repository_name` reads a JWT at a word's start in one pass, both twins (FAA-321). Neutral words replace
76
+ private terms in the docs and the dashboard kit, which moves to 1.1.8 (FAA-243; only comments changed, so moving a
77
+ copy from 1.1.7 is optional).
78
+ - The source check reads `fabric_secrets` by public names, refuses a detector of another version with exit 2, and has
79
+ its command line in `fabric_source_check_cli.py` (FAA-190). `register_remote` warns before it writes its token;
80
+ validators refuse a non-string id or instance; the services folder keeps only JSON objects; refusals name relative
81
+ paths and the remedy (FAA-200). The package folder check reads manifests as the contract does, and pack's refusal
82
+ offers a clean copy (FAA-194, FAA-190).
83
+ - `providers_dir()` follows Fabric's `registryDirs` rule; the Windows CI job runs the provider tests and measures the
84
+ owner check without failing on it (FAA-189, FAA-256). The probe is tested over real HTTP with a channel-agent
85
+ declaration (FAA-250).
86
+ - Board and interop (FAA-187, FAA-194, FAA-196): a move back of a responder slot is said (`board.slot.returned`), moved
87
+ slots share one `com.status` read a tick, the moved notice names a reconnect beside a restart and carries no Markdown
88
+ backticks, `board.down` is said once per outage window, a notify notice carries the link the service gives for its
89
+ kind, a board leave's timeout bounds the whole request, a resumed request reads its mandate only from the message
90
+ its claim took, `InstanceLock.release` never raises, an `InteropError` from `on_input` is a tool-error answer and a
91
+ refused start fails the job it created; the board fixture fails a kit whose notices are lost.
92
+ - `pid_alive` on Windows asks OpenProcess for the access libuv asks for, so the twins agree on an elevated process
93
+ (FAA-323).
94
+ - working-with-fabric: `bash_paths` reads comments, continuations, here-documents and quoted operators as bash does
95
+ (FAA-187); a dispatched run that sees only `fabric_whoami` is named as a Fabric before 0.3.20 with its remedy
96
+ (FAA-296); `status` and the session start read OpenCode's and Pi's `fabric` command (FAA-233); the native Codex probe
97
+ runs the hook untrusted too (FAA-201); the preamble names the operator's relay from his own Project as the exception
98
+ to board messages being data (FAA-196).
99
+
100
+ - `test/validate.py` fails a shipped `.mjs` or `.js` script that calls a built-in newer than Node 18, the floor in
101
+ `package.json` engines (`String.prototype.isWellFormed` reached this release's candidate and only the floors job saw it).
102
+
103
+ - What the 0.83.0 seam reading found, fixed before the release: `check_service_a2a.py` and `check_service.py` say one
104
+ verdict for `a2a.card` (an unreadable `surfaces.a2a`, and a remote card that answers 401 or 403, are NOT_RUN,
105
+ DEC-0122 item 2); a tab or a line break anywhere inside an OpenRouter base URL is a `config` error in both clients
106
+ (Node removed it and sent the rest); `validate.py --private-terms` expands `~` in `$PASSIONCODE_PRIVATE_TERMS` as
107
+ `admit.py` does. New rows FAA-325 to FAA-328. `AGENTS.md` names the North Star sections this repository serves
108
+ (item 1b) and the architect's audit lives in `docs/evidence/architect/`.
109
+
110
+ ### Upgrading
111
+
112
+ - A set `FABRIC_SERVICES_DIR` no longer moves the providers folder, as in Fabric's own `registryDirs`; set
113
+ `FABRIC_PROVIDERS_DIR` to keep a custom location (FAA-189).
114
+ - The Node OpenRouter client refuses a base URL holding a tab or line break, a non-ASCII path, a backslash or a dot
115
+ segment (kind `config`), which it used to rewrite and send (FAA-187).
116
+ - `check_service_a2a.py` is a command of its own: run it beside `check_service.py` for a service that serves A2A, with
117
+ `--key-env` naming the interface's own key (FAA-10).
118
+ - A service behind `tailscale serve` can check a person at each door with `fabric_tailnet_identity`: listen on
119
+ 127.0.0.1 only, give it the groups file's path (and, for the agents base's `door-groups/1`, the service's id), and
120
+ send its refusal at every door (FAA-324).
121
+
3
122
  ## 0.82.0 (2026-11-12)
4
123
 
5
124
  Wave 4 of the backlog sweep of 2026-11-12. The contract pin stays at `93e7db16` (`v0.1.0+2026-11-08.1`). Kit modules
@@ -1013,7 +1132,7 @@ on the Linux jobs (`python`, `floors`), so 0.69.1 is the first npm release carry
1013
1132
  ### Fixed
1014
1133
 
1015
1134
  - working-with-fabric: a run Fabric started on a board request closes its workflow only once the request is settled
1016
- (FAA-213, passioncode-vesta's finding on the board; Fabric agrees). On an error the run cannot get past (a refused
1135
+ (FAA-213, a board project's finding; Fabric agrees). On an error the run cannot get past (a refused
1017
1136
  claim, a missing key, a tool that fails twice) `references/on-demand-session.md` closed the workflow while the
1018
1137
  request could still be `queued` or `claimed`, and Fabric's next dispatch then failed at the take-back on every cycle
1019
1138
  (Fabric's main at `a6f0ffda`, before 0.3.16).
@@ -2094,7 +2213,7 @@ command's POSIX paths through Windows path rules. 0.53.1 is 0.53.0 with this fix
2094
2213
  ### Fixed
2095
2214
 
2096
2215
  - The hooks now name this release to Fabric: 0.51.1 still said 0.51.0, and `test/validate.py` now checks it.
2097
- Copylot check `4028969a-aa35-48f9-9300-331da036fb1d`: no findings.
2216
+ Text check `4028969a-aa35-48f9-9300-331da036fb1d`: no findings.
2098
2217
 
2099
2218
  ## 0.51.1 (2026-10-26)
2100
2219
 
@@ -2213,7 +2332,7 @@ release carrying everything listed under 0.51.0 below.
2213
2332
  its four strings were English only, so a Russian dashboard could not mount it without editing a vendored file that
2214
2333
  `dashboard_kit.py install` then refuses as changed. The message now uses a colon. The button reads the page's `lang`
2215
2334
  and shows Russian on a Russian page, English otherwise; `data-label-chat`, `data-label-session` (with `{name}`),
2216
- `data-label-open` and `data-label-absent` set a page's own words. The strings were checked with Copylot in the
2335
+ `data-label-open` and `data-label-absent` set a page's own words. The strings were checked with the text checker in the
2217
2336
  PassionCode.ai site voice (en-US report `239ff62c-e0af-4b8b-96e1-0c45c5eca02a`, ru-RU report
2218
2337
  `be37143c-aff1-4502-b9d0-f8b1be56bbf3`, no findings). A test fails on U+2014 in any kit asset, and the WebKit test
2219
2338
  opens a Russian page and a page with its own words. The button's accessible name follows the page too (*Открыть Fabric*, or
package/README.md CHANGED
@@ -89,8 +89,9 @@ retries, the agent's own key, a spend ceiling before every attempt, usage receip
89
89
  [connectors](plugins/fabric-agent-adapter/skills/building-fabric-services/references/connectors.md)); the draft-and-check step of a support agent as a
90
90
  [recipe](plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/README.md)
91
91
  (the whole agent starts from the [agents database](plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/agents-database.md)'s
92
- template when one is named (a closed database reached through Fabric's board, or by a grant holder from its own checkout; every ask is
93
- optional and never blocks), read beside creating-fabric-agents'
92
+ template when one is named (a closed database reached through Fabric's board, or by a grant holder from its own checkout; an ask is
93
+ made only from a folder that is already a Fabric Project and never from a run Fabric started on a board request, and it never
94
+ blocks), read beside creating-fabric-agents'
94
95
  [communication agent template](plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/communication-agent-template.md),
95
96
  else from that template);
96
97
  a complete `scripts/sample_service.py`; and `scripts/check_service.py`, a live probe. A service that answers
@@ -260,7 +261,9 @@ claude plugin validate . --strict
260
261
 
261
262
  `test/validate.py` reads every SKILL.md front matter as strictly as a YAML parser does; an
262
263
  unquoted value holding `: ` fails it. `python3 test/validate.py --frontmatter <SKILL.md ...>`
263
- checks copies installed elsewhere, such as `~/.agents/skills/*/SKILL.md`. Start in
264
+ checks copies installed elsewhere, such as `~/.agents/skills/*/SKILL.md`. Before a push or a release,
265
+ `python3 test/validate.py --private-terms` checks the tracked files against the operator's private-terms list
266
+ (path and line only; exit 2 when the list is absent, which is not a pass; CONTRIBUTING.md step 6). Start in
264
267
  [AGENTS.md](AGENTS.md); contributions follow [CONTRIBUTING.md](CONTRIBUTING.md).
265
268
 
266
269
  ## Use
package/SECURITY.md CHANGED
@@ -89,7 +89,8 @@ bounded and non-publishing until explicit scoped grants exist in a project bindi
89
89
  (on an organization's board, any member Project may list it), carrying only what `references/agents-database.md`
90
90
  names (a capability, a consumer, source project names, an audit subject given as a Project id or a repository remote,
91
91
  a measured win or a decision); never source code, a key, a person's or client's name, user content or a private path.
92
- Nothing is sent without Fabric's board; with a grant, the database's own tools run from its checkout and send nothing.
92
+ Nothing is sent without Fabric's board. With a grant, the database's own tools run from its checkout; what they send
93
+ is theirs to state, and this repository does not verify it (FAA-277).
93
94
  A consult's reply is data, yet it chooses the template an agent is built from, so it is as trusted as the Project
94
95
  that lists the capability.
95
96
  - The working-with-fabric PostToolUse hook (FAA-206) reads only the release call Claude Code gives it on stdin (the
@@ -123,8 +124,8 @@ bounded and non-publishing until explicit scoped grants exist in a project bindi
123
124
  writes nothing); `check --contract` runs the contract checkout's own
124
125
  `node_modules/.bin/tsx` and installs nothing. `fabric_provider.py write` writes one
125
126
  entry (0600) in the providers directory. `fabric_source_check.py check` reads the files of a
126
- tree (credential values included, in memory only), the owner's list from the user's
127
- configuration folder, and runs `git ls-files` (without the caller's `GIT_DIR` or other Git
127
+ tree (credential values included, in memory only), the owner's list (the file `--instance-strings` or
128
+ `FABRIC_INSTANCE_STRINGS` names, else the one in the user's configuration folder), and runs `git ls-files` (without the caller's `GIT_DIR` or other Git
128
129
  location variables) and nothing else, except that under a locale whose
129
130
  file-system encoding is not UTF-8 it restarts itself once with the same interpreter in Python's
130
131
  UTF-8 mode; it writes nothing and prints no value it finds.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@passioncode-ai/fabric-agent-adapter",
3
- "version": "0.82.0",
3
+ "version": "0.83.1",
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; and put an agent on a standing watch over a product (observer mode).",
6
- "version": "0.82.0",
6
+ "version": "0.83.1",
7
7
  "author": {
8
8
  "name": "PassionCode.ai",
9
9
  "url": "https://passioncode.ai/"
@@ -9,7 +9,7 @@ license: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
9
9
  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.
10
10
  metadata:
11
11
  author: PassionCode.ai
12
- version: "0.82.0"
12
+ version: "0.83.1"
13
13
  contract-version: "0.1.0"
14
14
  contract-commit: "93e7db164c7829e2783b4b7705c4113bb5201131"
15
15
  ---
@@ -186,7 +186,12 @@ default and a committed `*.example`; a key into the secret store, named only. Ru
186
186
  A value already committed stays in Git history: say so; a fresh history is the owner's act (no history rewrite, no
187
187
  force-push). Then lay down the source manifest, `fabric.source.json` (DEC-0149): `scripts/adapt_project.py source
188
188
  <project-root> --dry-run` proposes it, the author narrows `include` and names each key and store as a slot, and the
189
- check reads the source under it (the reference, *The source manifest*).
189
+ check reads the source under it (the reference, *The source manifest*). An existing agent keeps its data where it is
190
+ until its owner moves it: of the reference's data home, apply what 4c asks (no instance value in the source); the
191
+ data home and the per-release transfer audit are for an agent built new, and an existing one adopts them only when
192
+ its owner asks. An existing agent that answers people (support, sales, reviews) is compared, part by part, with
193
+ creating-fabric-agents' communication agent template and `references/agents-that-answer-people.md`: each part it
194
+ lacks is a finding in the report.
190
195
 
191
196
  ### 5. Make probes safe and meaningful
192
197
 
@@ -48,6 +48,16 @@ repository-relative path at a commit, an eval report). A decision without a numb
48
48
  | building-fabric-services | at each release | contribute the release's measured wins as success cases and its new technical decisions |
49
49
  | working-with-fabric | the retro after work | a lesson with a measured effect is also a success case: contribute it beside the lesson it records |
50
50
 
51
+ ## A template and the communication prompt
52
+
53
+ For an agent that talks with people on a channel, the database's template and creating-fabric-agents' communication
54
+ agent prompt (`references/communication-agent-template.md`) are read together, never as two templates (the database's
55
+ own rule). Where they disagree, the template wins, in every part and not only the first-release ones: it is the merged
56
+ practice of the agents of its kind. A part the prompt carries and the template lacks (a first-release part, a check, a
57
+ pipeline step) is built too, unless the template rules it out, and goes back to the database as feedback on that
58
+ template (`agents-db.contribute`); the completion report names each such part. Without a template for the kind, the
59
+ prompt is the recipe.
60
+
51
61
  ## Recording the template
52
62
 
53
63
  An agent built from a template declares it: `provenance.template` with its id and version, so a later template
@@ -12,7 +12,7 @@ directory:
12
12
  | macOS | `~/Library/Application Support/ai.passioncode.fabric/providers/` |
13
13
  | Linux | `${XDG_DATA_HOME:-~/.local/share}/passioncode-fabric/providers/` |
14
14
  | Windows | `%LOCALAPPDATA%\passioncode-fabric\providers\` (the same root as the services folder, DEC-0032; `~\AppData\Local` when `LOCALAPPDATA` is unset) |
15
- | any | `FABRIC_PROVIDERS_DIR` when set |
15
+ | any | `FABRIC_PROVIDERS_DIR` when set; `FABRIC_SERVICES_DIR` moves the services folder only, as Fabric's `registryDirs` reads them (FAA-189) |
16
16
 
17
17
  A service uses a descriptor instead (the `building-fabric-services` skill). One id is a
18
18
  service or a provider, never both.
@@ -25,7 +25,7 @@ and refuses what the contract refuses.
25
25
  ```bash
26
26
  python3 scripts/fabric_provider.py write --id example-agent --provider-id https://agents.example/providers/example-agent \
27
27
  --name "Example Agent" \
28
- --manifest ~/.local/share/example-agent/fabric-agent.json --installed-by "example-agent 1.0.0" \
28
+ --manifest '~/.local/share/example-agent/fabric-agent.json' --installed-by "example-agent 1.0.0" \
29
29
  --env EXAMPLE_API_KEY=secret-ref:example-agent/EXAMPLE_API_KEY \
30
30
  --stdio ~/.local/bin/example-agent mcp
31
31
  python3 scripts/fabric_provider.py write ... --url http://127.0.0.1:47201/mcp
@@ -33,6 +33,8 @@ python3 scripts/fabric_provider.py validate ~/Library/Application\ Support/ai.pa
33
33
  python3 scripts/fabric_provider.py remove example-agent # the uninstaller
34
34
  ```
35
35
 
36
+ The manifest is quoted so the entry keeps `~/`, which every Fabric release reads: unquoted, a shell expands it, and
37
+ Git Bash on Windows hands Python `C:/Users/...`, which Fabric v0.3.15 and earlier refuse (FAA-189).
36
38
  `--stdio` comes last and takes the rest of the line as the argument array; there is no shell string. A writer
37
39
  option after it (`--env`, `--name`, ...) is refused rather than swallowed into argv; write `--stdio -- EXECUTABLE ...`
38
40
  when the server itself takes an option of that name. `--env` belongs to a stdio server and is refused with `--url`. An entry may carry
@@ -54,7 +54,11 @@ source under it.
54
54
 
55
55
  - **The data has a home of its own from the first commit: `<agent>-data`.** For an agent that is not a kit service it
56
56
  is a folder beside the checkout, never inside it, and `<AGENT>_DATA_DIR` names it (its default is
57
- `../<agent>-data`); a kit service keeps the kit's folders above. When the owner keeps the data in Git, it goes to a
57
+ `../<agent>-data`, resolved against the checkout's root, the folder that holds the agent's source, never the working
58
+ directory). Two sibling checkouts, and a second context's instance from the same checkout (an organization's beside
59
+ the personal one), would share that default: each sets its own `<AGENT>_DATA_DIR` (`../<agent>-data.<instance>`), so
60
+ no copy reads another's data. A kit service keeps the kit's folders above, and on a host whose disk does not persist
61
+ `FABRIC_DATA_HOME` moves its data folder onto a durable volume (building-fabric-services, the remote placement). When the owner keeps the data in Git, it goes to a
58
62
  separate private repository named `<agent>-data`, never to the source's repository. On a cloud host that does not
59
63
  keep its disk across a restart (Heroku and the like, an uptime A agent's default), the data home is a durable store
60
64
  the same variable names (a database, an object store, that private repository); the folder and the kit's folders
@@ -114,13 +118,22 @@ untracked ones that are not ignored, as they are in the work tree; a staged copy
114
118
  | Kind | Finds |
115
119
  |---|---|
116
120
  | `home-path` | an absolute path inside a person's home folder (a web route under `/home/` is reported too; a route under `/users/` is not): `/Users/<name>/`, `/home/<name>/`, `C:\Users\<name>\`, Git Bash's `/c/Users/<name>/`, WSL's `/mnt/c/Users/<name>/`, and the dashed (`-Users-<name>-`, `C--Users-<name>-`) and URL-encoded forms a tool makes of one. The name is ASCII, and in the dashed form it ends at the next dash: an owner whose home name holds a space, a dash, a dot, an underscore or an apostrophe (each a dash in the dashed form; a slashed name also ends at a space) declares it with `home: <name>` in the owner's list, which reads it whole in every form, and one whose home name is not ASCII, or holds another character, lists it as a plain string (a typographic apostrophe a keyboard typed, which the folder's name does not hold, is written as `'` in `home:`; a folder whose name holds `’` is listed as a plain string with `’` kept and declared too with `home: <name>` written with `'`, which reads the dashed forms; its URL-encoded `’` is not read; a name outside ASCII of one or two characters cannot be checked) |
117
- | `secret` | a value shaped like a credential by the contract's detector (DEC-0044), read line by line: a provider key starting a word, a JWT, a bearer token, a long random value assigned to a credential's name (but a whole Ed25519 signature under a JSON `"sig"`, `"signature"` or `"signatures"` member, as `sign` writes it, which is public); or a private key: a PEM, OpenSSH or PGP block, or a JWK's `d` as a JSON member, escaped JSON or a Python dict; a URL with a password of 8 or more characters mixing letters and digits (a password without a digit or without a letter is not read) or a credential in its query, percent-encoded or not, JSON-escaped (`\u0026`, `\/`) or nested in another URL's parameter (two levels deep); a file that is a credential by its name, as `pack` refuses it (`.env`, `*.pem`, `*.key`, `.npmrc` and the rest), or a kit credential file (`*.token`, `board.credential`) |
121
+ | `secret` | a value shaped like a credential by the contract's detector (DEC-0044), read line by line: a provider key starting a word, a JWT, a bearer token, a long random value assigned to a credential's name (a run of 40 or more characters mixing upper case, lower case and digits, so a shorter key under `X_API_KEY=` is not read; but a whole Ed25519 signature under a JSON `"sig"`, `"signature"` or `"signatures"` member, as `sign` writes it, which is public); or a private key: a PEM, OpenSSH or PGP block, or a JWK's `d` as a JSON member, escaped JSON or a Python dict; a URL with a password of 8 or more characters mixing letters and digits (a password without a digit or without a letter is not read) or a credential in its query, percent-encoded or not, JSON-escaped (`\u0026`, `\/`) or nested in another URL's parameter (two levels deep); a file that is a credential by its name, as `pack` refuses it (`.env`, `*.pem`, `*.key`, `.npmrc` and the rest), or a kit credential file (`*.token`, `board.credential`) |
118
122
  | `instance-string` | a string from the owner's list, in a file, in a file's path or in a link's target |
119
123
 
120
124
  ```bash
121
125
  python3 <plugin-dir>/skills/adapting-projects-to-fabric/scripts/fabric_source_check.py check <project-root>
122
126
  ```
123
127
 
128
+ Run it by path, with its scripts folder whole: it loads its siblings (`fabric_source_ignore.py`,
129
+ `fabric_source_homes.py`, `fabric_source_restart.py`, `fabric_source_check_cli.py`, `fabric_source_tree.py` and the
130
+ contract's detector `fabric_secrets.py`) and refuses with exit `2` when one is missing or from another version, a
131
+ detector without one of the public names it reads included (FAA-190). `python3 -m fabric_source_check` writes its own
132
+ `.pyc` before the check turns bytecode off; run by path, it writes nothing. `ROOT` defaults to the current folder.
133
+ `--json` prints one object: `mode` (`git` or a walk), `files`, `findings` (`path`, `line`, `kind`, `rule`),
134
+ `notRead`, `skipped`, `dependencies`, `instanceStrings`, `ownerHomes`, `instanceList`, `blocking` (a finding) and
135
+ `complete` (every file read); `--help` names each option.
136
+
124
137
  - It **never repeats** a value it found: no value, no line holding one, no entry of the owner's list and not the
125
138
  folder it checks reaches its output. A file's path is checked like its text (line `0` of a finding), and printed
126
139
  with an entry of the list as `<line N>`, a home folder's name as `<home>` and each segment holding a
@@ -167,7 +180,7 @@ python3 <plugin-dir>/skills/adapting-projects-to-fabric/scripts/fabric_source_ch
167
180
  percent-encodings or an escaped apostrophe (`\'`, `\x27`, `\u0027`, `&#39;`, `&#x27;`, `&apos;`), the same separators it is found with in every form; a home that is also your name goes in the list
168
181
  as a plain string too, to be found in text. It is `--instance-strings FILE`, else the file
169
182
  `FABRIC_INSTANCE_STRINGS` names, else `passioncode-fabric/instance-strings.txt` in the user's configuration folder
170
- (`$XDG_CONFIG_HOME` when it is absolute, else `~/.config`; `%APPDATA%` on Windows). A list inside the tree is refused, because it would
183
+ (`$XDG_CONFIG_HOME` when it is absolute, else `~/.config`; on Windows `%APPDATA%` when it is absolute, else `~\AppData\Roaming`). A list inside the tree is refused, because it would
171
184
  travel with the source. A person who already keeps private terms for org-index's `check_private.py`
172
185
  (`~/.config/passioncode/private-terms.txt`) passes that file with `--instance-strings`; this check matches a term
173
186
  anywhere in a word, and it does not look for e-mail addresses or account ids by their shape.
@@ -183,7 +196,8 @@ python3 <plugin-dir>/skills/adapting-projects-to-fabric/scripts/fabric_source_ch
183
196
  repository is not read; `--every-file` and a folder outside Git read its files, its `.git` left out. An exception line (`!`) is refused, since what it would
184
197
  bring back would go unread. Name the files, or a folder that holds
185
198
  only synthetic vectors, never a whole source folder (a nested repository checked in its own tree is the one
186
- exception), and say why on the line above. The `LICENSE` is the other: its copyright line names the holder, which is
199
+ exception), and say why on the line above. The one owner's file named this way is the `LICENSE` (a file, never a
200
+ folder exception): its copyright line names the holder, which is
187
201
  authorship and travels with the source, so when the holder is on the owner's list the licence file is named in the ignore
188
202
  file by its own path (`LICENSE`, `LICENSE.md`) with that reason (`# the copyright holder is authorship and travels with the source`); no other file of the
189
203
  owner's is. A file `pack` refuses by its name cannot ship in a package: a public
@@ -451,6 +451,20 @@ def _declared_effect(args: argparse.Namespace) -> Tuple[str, str]:
451
451
  return effect, idempotency
452
452
 
453
453
 
454
+ #: What running the kit leaves in a scaffold, which the source check reads as binary in Git mode (FAA-190 item 36).
455
+ GITIGNORE_LINES = ("__pycache__/", "*.pyc")
456
+
457
+
458
+ def _gitignore(root: Path, write: bool) -> str:
459
+ """`.gitignore` holds GITIGNORE_LINES, created or with the missing lines appended (nothing else changes)."""
460
+ path = root / ".gitignore"
461
+ text = path.read_text(encoding="utf-8", errors="replace") if path.exists() else None
462
+ missing = "".join(line + "\n" for line in GITIGNORE_LINES if line not in (text or "").splitlines())
463
+ if missing and write:
464
+ path.write_text((text or "") + ("\n" if text and not text.endswith("\n") else "") + missing, encoding="utf-8")
465
+ return "unchanged" if not missing else "created" if text is None else "appended"
466
+
467
+
454
468
  def scaffold_project(root: Path, args: argparse.Namespace) -> Dict[str, Any]:
455
469
  if not root.is_dir():
456
470
  raise AdaptationError("project path is not a directory: %s" % root)
@@ -472,6 +486,7 @@ def scaffold_project(root: Path, args: argparse.Namespace) -> Dict[str, Any]:
472
486
  "wouldReplace": sorted(collisions),
473
487
  "needsForce": bool(collisions) and not args.force,
474
488
  "diffs": diffs,
489
+ "gitignore": _gitignore(root, write=False),
475
490
  }
476
491
  if collisions and not args.force:
477
492
  raise AdaptationError("refusing to overwrite existing paths: %s" % ", ".join(collisions))
@@ -484,6 +499,7 @@ def scaffold_project(root: Path, args: argparse.Namespace) -> Dict[str, Any]:
484
499
  "profile": args.profile,
485
500
  "created": sorted(generated),
486
501
  "replaced": sorted(collisions),
502
+ "gitignore": _gitignore(root, write=True),
487
503
  "next": "Replace all placeholders, implement the adapter, then run check with the pinned contract checkout.",
488
504
  }
489
505
 
@@ -93,26 +93,29 @@ class ProviderError(Exception):
93
93
  """An entry was refused; the message is one readable sentence and never quotes a value."""
94
94
 
95
95
 
96
- def services_dir() -> Path:
97
- override = os.environ.get("FABRIC_SERVICES_DIR")
98
- if override:
99
- return Path(override).expanduser()
96
+ def fabric_root() -> Path:
97
+ """The platform's Fabric root, as Fabric's registryDirs computes it: the folder `services/` and `providers/` sit in
98
+ when neither override is set."""
100
99
  home = Path.home()
101
100
  if sys.platform == "darwin":
102
- return home / "Library/Application Support/ai.passioncode.fabric/services"
101
+ return home / "Library/Application Support/ai.passioncode.fabric"
103
102
  if sys.platform == "win32":
104
103
  # provider.md "Where the directory is" (DEC-0032): LOCALAPPDATA, never the roaming APPDATA.
105
104
  local = os.environ.get("LOCALAPPDATA")
106
- return (Path(local) if local else home / "AppData" / "Local") / "passioncode-fabric" / "services"
107
- return Path(os.environ.get("XDG_DATA_HOME") or str(home / ".local/share")) / "passioncode-fabric/services"
105
+ return (Path(local) if local else home / "AppData" / "Local") / "passioncode-fabric"
106
+ return Path(os.environ.get("XDG_DATA_HOME") or str(home / ".local/share")) / "passioncode-fabric"
107
+
108
+
109
+ def services_dir() -> Path:
110
+ override = os.environ.get("FABRIC_SERVICES_DIR")
111
+ return Path(override).expanduser() if override else fabric_root() / "services"
108
112
 
109
113
 
110
114
  def providers_dir() -> Path:
111
- """FABRIC_PROVIDERS_DIR, else `providers/` in the same root as `services/`."""
115
+ """FABRIC_PROVIDERS_DIR, else `providers/` in the platform's Fabric root, as Fabric's registryDirs reads it: a set
116
+ FABRIC_SERVICES_DIR moves the services folder only (FAA-189 (2))."""
112
117
  override = os.environ.get("FABRIC_PROVIDERS_DIR")
113
- if override:
114
- return Path(override).expanduser()
115
- return services_dir().parent / "providers"
118
+ return Path(override).expanduser() if override else fabric_root() / "providers"
116
119
 
117
120
 
118
121
  def looks_like_credential(value: str) -> bool:
@@ -155,7 +158,7 @@ def validate_provider_entry(entry: Any) -> List[str]:
155
158
  # FAA-120, as the service kit: a repository is named, never reached with a credential in its URI. Said
156
159
  # before the character check, so a password beside a character the URI class refuses (a backslash, as
157
160
  # git reads one) is still named a credential (FAA-127).
158
- problems.append("source.repository carries no password or credential: it names the repository")
161
+ problems.append("source.repository carries no password or credential: it names the repository (write the repository's name in lower case if it is one)")
159
162
  elif "repository" in source and not _is_uri(source["repository"]):
160
163
  problems.append("source.repository is an absolute URI")
161
164
  if not isinstance(entry["installedAt"], str) or not _TIMESTAMP.fullmatch(entry["installedAt"]):
@@ -207,12 +210,17 @@ def _run_problems(run: Any) -> List[str]:
207
210
  return problems
208
211
 
209
212
 
213
+ def _no_constant(name: str) -> Any:
214
+ raise ValueError("%s is no JSON" % name)
215
+
216
+
210
217
  def manifest_problems(entry: Dict[str, Any]) -> List[str]:
211
218
  """FAC-SEM-014 (DEC-0017), URI half: the manifest resolves and its provider.id equals providerId, compared by normal
212
219
  form (DEC-0076)."""
213
220
  try:
214
- manifest = json.loads(Path(os.path.expanduser(str(entry["manifest"]))).read_text(encoding="utf-8"))
215
- except (OSError, ValueError):
221
+ manifest = json.loads(Path(os.path.expanduser(str(entry["manifest"]))).read_text(encoding="utf-8"),
222
+ parse_constant=_no_constant)
223
+ except (OSError, ValueError, RecursionError): # NaN or Infinity is no JSON, as pack reads it (FAA-194 (38))
216
224
  return ["the manifest %s does not resolve (FAC-SEM-014)" % entry["manifest"]]
217
225
  provider = manifest.get("provider") if isinstance(manifest, dict) else None
218
226
  if not isinstance(provider, dict) or not isinstance(manifest.get("capabilities"), list):
@@ -300,6 +308,14 @@ def entry_stem(path: str) -> str:
300
308
  return re.sub(r"\.json\Z", "", re.sub(r"\A[^\n\r\u2028\u2029]*[\\/]", "", path, count=1))
301
309
 
302
310
 
311
+ def argument_stem(path: str, platform: Optional[str] = None) -> str:
312
+ """The stem of the file a `validate` argument names: CT-193's, after a Windows drive-relative prefix (`C:name.json`
313
+ is name.json in that drive's folder) is set aside, which the registry never meets (FAA-189 (8))."""
314
+ if (platform or sys.platform) == "win32":
315
+ path = re.sub(r"\A[A-Za-z]:(?![\\/])", "", path)
316
+ return entry_stem(path)
317
+
318
+
303
319
  def main(argv: Optional[List[str]] = None) -> int:
304
320
  parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
305
321
  sub = parser.add_subparsers(dest="command", required=True)
@@ -335,7 +351,7 @@ def main(argv: Optional[List[str]] = None) -> int:
335
351
  entry = json.loads(args.path.read_text(encoding="utf-8"))
336
352
  problems = validate_provider_entry(entry)
337
353
  # CT-193 (FAC-SEM-014): the stem is what follows the last / or \ and loses .json, on every platform.
338
- stem = entry_stem(str(args.path))
354
+ stem = argument_stem(str(args.path))
339
355
  if isinstance(entry, dict) and entry.get("id") != stem:
340
356
  problems.append("the file name %s is not <id>.json for %s (FAC-SEM-014)" % (args.path.name, entry.get("id")))
341
357
  if not problems:
@@ -31,7 +31,7 @@ CREDENTIAL_PAIRS: Dict[str, tuple] = {
31
31
  }
32
32
  #: ECMAScript's \s, as a character class body.
33
33
  JS_SPACE = "\t\n\x0b\x0c\r \xa0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff"
34
- _NOT_LINE_END = "[^\n\r\u2028\u2029]" # ECMAScript's `.` without the s flag
34
+ NOT_LINE_END = "[^\n\r\u2028\u2029]" # ECMAScript's `.` without the s flag
35
35
  JWT_SHAPE = re.compile(r"eyJ[A-Za-z0-9_-]{2,}\.[A-Za-z0-9_-]{2,}\.[A-Za-z0-9_-]*")
36
36
  _URL_BASE64_RUN = re.compile(r"[A-Za-z0-9_-]*")
37
37
 
@@ -74,14 +74,17 @@ _PROVIDER_KEYS = (r"(?:sk-(?=[A-Za-z0-9_-]{0,255}?[0-9])(?:(?=[A-Za-z0-9_-]{0,25
74
74
  #: The contract's SECRET_VALUE at the pin (DEC-0151): `Bearer ` at a value's start in any case, or a provider key after a
75
75
  #: boundary. No case-insensitive flag; digits are ASCII. A match includes its boundary character: callers test, never
76
76
  #: read the matched text or its index.
77
- SECRET_VALUE = re.compile("(^[Bb][Ee][Aa][Rr][Ee][Rr][" + JS_SPACE + "]" + _NOT_LINE_END + "+|" + _KEY_BOUNDARY + _PROVIDER_KEYS + ")")
77
+ SECRET_VALUE = re.compile("(^[Bb][Ee][Aa][Rr][Ee][Rr][" + JS_SPACE + "]" + NOT_LINE_END + "+|" + _KEY_BOUNDARY + _PROVIDER_KEYS + ")")
78
78
  _LONG_RUN = re.compile(r"[A-Za-z0-9+/=_-]{40,}")
79
- _BEARER_INSIDE = re.compile(r"(?<![A-Za-z0-9_])bearer[%s]+(?=[A-Za-z0-9._~+/-]*[0-9])[A-Za-z0-9._~+/-]{12,}=*" % JS_SPACE,
79
+ BEARER_INSIDE = re.compile(r"(?<![A-Za-z0-9_])bearer[%s]+(?=[A-Za-z0-9._~+/-]*[0-9])[A-Za-z0-9._~+/-]{12,}=*" % JS_SPACE,
80
80
  re.IGNORECASE | re.ASCII)
81
81
  _PATH_LIKE = re.compile(r"^(?:[/~.]|[A-Za-z]:[\\/]|[a-z][a-z0-9+.-]*://)", re.IGNORECASE | re.ASCII)
82
82
  _WORD_SPLIT = re.compile("[%s._-]+" % JS_SPACE)
83
83
  _ARGUMENT_SPLIT = re.compile("[%s=]" % JS_SPACE)
84
- _SCHEME = re.compile(r"^[A-Za-z][A-Za-z0-9+.-]*:")
84
+ SCHEME = re.compile(r"^[A-Za-z][A-Za-z0-9+.-]*:")
85
+ #: The names the check and older readers wrote before these became public (FAA-190 (2), (12)): kept so a module from
86
+ #: an earlier version beside this one still loads.
87
+ _BEARER_INSIDE, _SCHEME, _NOT_LINE_END = BEARER_INSIDE, SCHEME, NOT_LINE_END
85
88
  #: WHATWG's special schemes that carry an authority with user information (file has none).
86
89
  _SPECIAL = ("ftp", "http", "https", "ws", "wss")
87
90
 
@@ -108,13 +111,17 @@ def credential_name(name: str) -> bool:
108
111
  return any(word in words and any(p in words for p in partners) for word, partners in CREDENTIAL_PAIRS.items())
109
112
 
110
113
 
111
- def _long_secret(value: str) -> bool:
114
+ def long_secret(value: str) -> bool:
115
+ """A run of 40 or more base64 characters mixing upper case, lower case and digits (the Node twin's longSecret)."""
112
116
  return any(re.search(r"[a-z]", run) and re.search(r"[A-Z]", run) and re.search(r"[0-9]", run) for run in _LONG_RUN.findall(value))
113
117
 
114
118
 
119
+ _long_secret = long_secret # its name before FAA-190 (2)
120
+
121
+
115
122
  def secret_value(value: str) -> bool:
116
123
  """A JWT anywhere, a provider key or `Bearer …`, or a long base64 run mixing upper case, lower case and digits."""
117
- return bool(has_jwt(value) or SECRET_VALUE.search(value) or _long_secret(value))
124
+ return bool(has_jwt(value) or SECRET_VALUE.search(value) or long_secret(value))
118
125
 
119
126
 
120
127
  def _path_like(piece: str) -> bool:
@@ -126,13 +133,13 @@ def _path_like(piece: str) -> bool:
126
133
  def secret_argument(value: str) -> bool:
127
134
  """The same for a launch argument or a server's command and URL: a long run is looked for between path separators,
128
135
  so a folder path holding a session id is not taken for a key."""
129
- if has_jwt(value) or SECRET_VALUE.search(value) or _BEARER_INSIDE.search(value):
136
+ if has_jwt(value) or SECRET_VALUE.search(value) or BEARER_INSIDE.search(value):
130
137
  return True
131
138
  for piece in _ARGUMENT_SPLIT.split(value):
132
139
  if _path_like(piece):
133
- if any(_long_secret(part) for part in re.split(r"[\\/]", piece)):
140
+ if any(long_secret(part) for part in re.split(r"[\\/]", piece)):
134
141
  return True
135
- elif _long_secret(piece):
142
+ elif long_secret(piece):
136
143
  return True
137
144
  return False
138
145
 
@@ -421,7 +428,7 @@ def _url_credential(value: str, hosts: bool) -> bool:
421
428
  # What WHATWG URL parsing does first: trim C0 controls and space from the ends (never other Unicode spaces, which it
422
429
  # refuses), then remove every tab, CR and LF.
423
430
  value = re.sub(r"[\t\n\r]", "", value.strip(_C0_OR_SPACE))
424
- if not _SCHEME.match(value):
431
+ if not SCHEME.match(value):
425
432
  return False # not an absolute URL (WHATWG URL parsing refuses it)
426
433
  if hosts and url_refused(value):
427
434
  return False # `new URL` refuses its host or port, so the Node twin reads it as an argument only (FAA-309)
@@ -442,7 +449,25 @@ _REPOSITORY_AUTHORITY = re.compile(r"[A-Za-z][A-Za-z0-9+.-]*://")
442
449
  #: boundary, so composed after a separator look-behind it would miss a key after `-`, `.` or `_`.
443
450
  _REPOSITORY_KEYS = r"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,}"
444
451
  _KEY_AT_A_WORD = re.compile(r"(?:\A|(?<=[._-]))(?:%s)" % _REPOSITORY_KEYS)
445
- _JWT_AT_A_WORD = re.compile(r"(?:\A|(?<=[._-]))%s" % JWT_SHAPE.pattern)
452
+
453
+
454
+ def jwt_at_a_word(text: str) -> bool:
455
+ """Whether JWT_SHAPE matches at a word's start: the text's start, or after a `.`, `_` or `-` (FAA-321). Read in one
456
+ pass, as jwt_spans: the look-behind expression backtracked from every such `eyJ` of a long run, quadratic in it.
457
+ Every `eyJ` of one run shares that run's end and a shorter first segment, so only a run's first `eyJ` at a word's
458
+ start is tried; an `eyJ` that is not at one is passed over, since a later one of its run may be."""
459
+ at = text.find("eyJ")
460
+ while at >= 0:
461
+ if at and text[at - 1] not in "._-":
462
+ at = text.find("eyJ", at + 1)
463
+ continue
464
+ one = _URL_BASE64_RUN.match(text, at + 3).end()
465
+ if one - at - 3 >= 2 and text[one:one + 1] == ".":
466
+ two = _URL_BASE64_RUN.match(text, one + 1).end()
467
+ if two - one - 1 >= 2 and text[two:two + 1] == ".":
468
+ return True
469
+ at = text.find("eyJ", one)
470
+ return False
446
471
 
447
472
 
448
473
  def repository_name(piece: str) -> bool:
@@ -450,7 +475,7 @@ def repository_name(piece: str) -> bool:
450
475
  key's or JWT's shape (`sk-`, `ghp_`, `xoxb-`, `AKIA`, `eyJ`) at its start or after a `.`, `_` or `-` (0.62.0
451
476
  readings 9 and 10), so a letters-only key is still a key while `task-pipeline-dashboard`, which holds an `sk-` shape
452
477
  inside a word, is a name."""
453
- return bool(_REPOSITORY_NAME.match(piece)) and not _KEY_AT_A_WORD.search(piece) and not _JWT_AT_A_WORD.search(piece)
478
+ return bool(_REPOSITORY_NAME.match(piece)) and not _KEY_AT_A_WORD.search(piece) and not jwt_at_a_word(piece)
454
479
 
455
480
 
456
481
  def ascii_decoded(text: str) -> str: