@passioncode-ai/fabric-agent-adapter 0.56.0 → 0.58.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/CHANGELOG.md +108 -0
  2. package/COMMERCIAL-LICENSE.md +1 -1
  3. package/README.md +18 -7
  4. package/SECURITY.md +6 -1
  5. package/package.json +1 -1
  6. package/plugins/fabric-agent-adapter/.claude-plugin/plugin.json +1 -1
  7. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/SKILL.md +18 -4
  8. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/provider-entry.md +18 -7
  9. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/source-and-instance.md +154 -0
  10. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/adapt_project.py +2 -2
  11. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_provider.py +25 -5
  12. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_source_check.py +995 -0
  13. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_source_homes.py +72 -0
  14. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_source_ignore.py +185 -0
  15. package/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +4 -3
  16. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/delegation.md +3 -1
  17. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/interop.md +5 -1
  18. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/package.md +22 -0
  19. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/protocol.md +5 -3
  20. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-actions.mjs +2 -1
  21. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-protocols.mjs +5 -3
  22. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_actions.py +2 -1
  23. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_protocols.py +4 -2
  24. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/kit.json +185 -5
  25. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +16 -7
  26. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/agents-that-answer-people.md +11 -5
  27. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/source-and-instance.md +154 -0
  28. package/plugins/fabric-agent-adapter/skills/working-with-fabric/SKILL.md +9 -9
  29. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_env.py +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,113 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.58.0 (2026-11-03)
4
+
5
+ Portable agents, part 1 (FAA-188, RM-36): an agent keeps its source apart from its instance from its first commit, and
6
+ a local check finds instance data in a source tree without repeating it. The manifest of a source, its export and
7
+ import wait for the contract's decision on CT-194 (part 2).
8
+
9
+ ### Added
10
+
11
+ - **`fabric_source_check.py check`** (adapting-projects-to-fabric) reads the files a source would carry (what Git would
12
+ commit; every file but the dependency folders it names when Git lists none; or with `--every-file`, which reads
13
+ what `pack` carries) and reports, by path, line and kind: a path inside a person's home folder
14
+ (`home-path`: an ASCII home name in the macOS, Linux (`/home`, `/var/home`) and Windows forms (Git Bash's `/c/Users`
15
+ and WSL's `/mnt/c/Users` included), the dashed form of DEC-0105 (`C--Users-` on Windows; a name holding a space, a dash, a dot, an underscore or an apostrophe is declared with `home: <name>` to be read whole there) and the URL-encoded ones; a web route `/users/...`, a flag `--home-dir` or a CSS variable is not one), a
16
+ value shaped like a credential by the contract's detector read line by line, a long random value under a
17
+ credential's name (a whole Ed25519 signature under a JSON `"sig"`, `"signature"` or `"signatures"` member, as `sign` writes it, is public and not one; a storage URL's `sig=` is) or a private key: a PEM, OpenSSH or PGP block, or a JWK's `d` (JSON, escaped JSON or a Python dict), a URL carrying a password (8 or more characters with at least one letter and one digit) or a credential in its query (percent-encoded, JSON-escaped or nested), and a
18
+ file that is a credential by its name as `pack` refuses it, or a kit credential file (`*.token`,
19
+ `board.credential`) (`secret`), and a string from the owner's list of instance strings, kept
20
+ outside the tree in the user's configuration folder (`instance-string`, in a file, its path or a link's target). It
21
+ never prints a value it found, a line that holds one, an entry of the owner's list or the folder it checks: a file's
22
+ path is checked like its text and printed with an entry as `<line N>`, a home name as `<home>` and a segment holding a
23
+ credential-shaped value as `<secret>`. Every text file is read whole; a binary, unreadable or (in Git mode) nested-repository path is listed as not
24
+ read (check a nested repository in its own tree, then name it in the ignore file; before `pack`, check it with
25
+ `--every-file` too). It changes nothing: exit 0 with no finding and every file read, 1 with a finding, 2 when it cannot run or a file
26
+ was not read (a binary file counts unless the ignore file names it as source). `.fabric-source-ignore` and
27
+ `--exclude` leave out named files of synthetic test values (and a project `.npmrc` that holds no credential: `pack`
28
+ still refuses it, so the package is a copy without it), and the summary counts them. A line is a path from the
29
+ root, not as `.gitignore` reads one: a line with no `/` matches at the root only, a wildcard line takes only what
30
+ it matches (`*` stays in a folder, `**` crosses folders), and a line ending in `/`, or a path with no wildcard,
31
+ takes the folder; a nested repository or a submodule is left out only by a line naming its folder (`vendor/sub/`),
32
+ never by `vendor/*`, and a wildcard folder line (`vendor/*/`) takes every one it reaches, checked or not, so name
33
+ each by its path; an exception line (`!`), a line holding `\`, `//`, an unclosed `[` or a POSIX class, and a
34
+ glob it cannot read are refused. Git is run as `git ls-files` with its fsmonitor off and without the caller's `GIT_DIR` or index, taken only from PATH's
35
+ absolute folders (with PATH unset, `/bin` and `/usr/bin`; on Windows only a `git.exe`, and none with PATH unset). Under a locale whose file-system encoding is not UTF-8 it restarts itself in Python's UTF-8 mode, so names, arguments and text are read in one encoding; an owner's entry is compared composed (NFC), so it finds a name macOS stored decomposed. A neutral home (container, cloud and CI users, first names such as `alice` and accounts such as `admin`; an owner whose own home is a first name, an account or one or two letters writes `home: <name>` in the owner's list, the whole name, spaces included, which is then read in every form, while a container's, a cloud image's or a CI runner's user that only the image uses, such as `node` or `vscode`, is never declared; the owner's own machine's home is declared even when its name is an image user's (`ubuntu`, `admin`), and an image's paths under it then go through a build argument that names the user; a folder holding a typographic apostrophe is listed as a plain string with it kept and declared too with `home:` written with `'`) and
36
+ a value that says it is synthetic are not findings. Standard library only; the adapter's own tree passes it, with its
37
+ contract vectors, eight test files and its icon named in its `.fabric-source-ignore` (`test_source_check`).
38
+ `building-fabric-services/references/package.md` runs it with `--every-file` before `pack`, on a clean copy it
39
+ describes: what Git would commit and the nested trees checked on their own, each committed symbolic link replaced
40
+ by its committed target or removed and reviewed, shared source copied in and committed, dependency folders and
41
+ what `pack` refuses left out; SECURITY.md names what it reads.
42
+ - **references/source-and-instance.md** (both skills): the layout (instance values in a configuration outside the
43
+ repository with a neutral default and a committed `*.example`, records and state documents in the kit's data folder,
44
+ keys by name, fixtures on neutral ids; the supervisor label and `createdBy` name the publisher until CT-194) and the
45
+ check. It is registered as a shared reference (`test_shared_references`).
46
+ - creating-fabric-agents step 2 lays an agent out as source and instance and calls the skeleton done, once the owner has reviewed it (a floor, not a secret scanner), when the check
47
+ exits 0; step 5 runs it again. adapting-projects-to-fabric step 4c runs the check first, moves each finding out and
48
+ runs it again; a value already in Git history needs a fresh history, which stays the owner's act. Step 7's report
49
+ names a contract checkout by its commit, never a home path, and the check runs again after it. The check's ignore-line
50
+ reading is `fabric_source_ignore.py` and its reading of a declared home `fabric_source_homes.py`, both beside it, and the owner's list takes `home: <name>` for the owner's own home name.
51
+ - Trigger and scenario evals for both (`source-apart-from-instance`, `move-instance-data-out-of-an-existing-agent`);
52
+ the Windows CI job runs `test_source_check`; the release smoke checks the script is installed. README, AGENTS.md and
53
+ SKILL-CARD name it.
54
+
55
+ ### Changed
56
+
57
+ - `COMMERCIAL-LICENSE.md` is the knowledge base template as of fabric-workspace `875faae2` (line 5 now uses a comma
58
+ where it had a dash); the hash `test/validate.py` pins moved with it. The licence terms are unchanged.
59
+
60
+ ### Upgrading
61
+
62
+ - Nothing to change: the check is new and opt-in. Handing a source over (a new remote, a push, a change of
63
+ visibility) stays the owner's act each time; the check reads the tree, never Git history.
64
+
65
+ ## 0.57.0 (2026-10-28)
66
+
67
+ The contract pin moves to `3830a33` (`v0.1.0+2026-10-27.6`) from `00e050ba` (FAA-176).
68
+
69
+ ### Added
70
+
71
+ - **`comparison` as a human step** (DEC-0142, contract CT-186). `humanStep` in an action declaration may be
72
+ `comparison`: the person compares two codes shown on two screens, or chooses to share without a comparison. Both
73
+ kits' action-declaration checks take it (`fabric_actions.py`, `fabric-actions.mjs`, `actions-vectors.json`
74
+ regenerated: 68 cases, 12 declarations), the probe passes a declaration that uses it, and interop.md, delegation.md
75
+ and working-with-fabric's human gates (OA-5), which its refusal table cites, name it. A reader pinned before (a 0.56.0
76
+ kit, Fabric Dashboards before 0.8.9) refuses such a declaration, which Project Observatory declares on its share
77
+ buttons from OBS-264.
78
+ - **The protocol register** gains the six peer-share revisions (DEC-0142, `fabric-peer-share/1` and its parts) and
79
+ `org-adaptation/1` and `org-adaptation-receipt/1` (DEC-0138) in both twins (`feed-protocols-vectors.json`), so a
80
+ service's well-known that names one is read as a known revision.
81
+ - **The provider entry's manifest path** (DEC-0145, CT-192): `fabric_provider.py` names the schema's
82
+ `MANIFEST_PATTERN`, read as a search that ends at the string's end; the reader accepts the manifest's name after a
83
+ backslash, the writer still writes it with `/` and refuses the other with a sentence naming CT-192, and the CLI's
84
+ stem is read after either separator (CT-193). The contract's ten `provider.schema.json` fixtures are vendored
85
+ (`provider-fixtures.json`) and tested in every run.
86
+
87
+ ### Changed
88
+
89
+ - On Windows, `fabric_provider.py`'s `services_dir()` (its FAC-SEM-013 check that an id is not a service's) and
90
+ `providers_dir()` read `%LOCALAPPDATA%\passioncode-fabric` (provider.md), as the service kit's readers already did.
91
+ - agents-that-answer-people.md (A5) names the contract's `support-quality-daily.schema.json`, says when a producer may
92
+ carry `calibration` (CT-188) and that a reader validates against its own pinned copy (DEC-0144).
93
+ - The hosted contract job runs `test_provider`, and `test/validate.py` fails a workflow whose contract job leaves out a
94
+ module that holds a test gated on the contract.
95
+
96
+ ### Upgrading
97
+
98
+ - A bundle pinned to `00e050ba` or an earlier supported commit keeps working; `adapt_project.py`
99
+ accepts it (`SUPPORTED_CONTRACT_COMMITS`). A new scaffold writes `3830a33`.
100
+ - DEC-0139 (the machines' report) and DEC-0138's organization server route reach no kit code: the adapter registers
101
+ their protocol revisions and implements neither side. DEC-0143 (`secret.prompt`, the local channel) and DEC-0142's
102
+ peer-share profile beyond its register entries reach no kit code either; Project Observatory implements them
103
+ (CT-186).
104
+ - On Windows, a provider entry written by `fabric_provider.py` now goes to the folder Fabric reads
105
+ (`%LOCALAPPDATA%\passioncode-fabric\providers`). Fabric v0.3.15 and earlier read a manifest path only under `~/` or
106
+ `/`; a drive letter (`C:/.../fabric-agent.json`) is read by the Fabric release after v0.3.15 (CO-479). Until then
107
+ write the manifest path under `~/`. An entry an older kit wrote on Windows sits in
108
+ `%USERPROFILE%\.local\share\passioncode-fabric\providers`, which Fabric never read and `remove` no longer finds:
109
+ write it again and delete the old file.
110
+
3
111
  ## 0.56.0 (2026-10-28)
4
112
 
5
113
  Stability and Windows: the debts of 0.53.0 to 0.55.0 that touch how the hooks, the board responder and the interop
@@ -2,7 +2,7 @@
2
2
 
3
3
  This repository is open source under the [GNU Affero General Public License v3.0](LICENSE)
4
4
  (`AGPL-3.0-only`). PassionCode.ai also offers it under a commercial license, for use that does not
5
- meet the AGPL's terms — for example inside a closed-source product, or as a modified hosted
5
+ meet the AGPL's terms, for example inside a closed-source product, or as a modified hosted
6
6
  service whose source is not published.
7
7
 
8
8
  To obtain a commercial license, tell us about your use at **https://passioncode.ai/business/**, or write to **commercial@passioncode.ai**.
package/README.md CHANGED
@@ -124,7 +124,10 @@ For provider bundles the adapting skill helps an agent author:
124
124
  - scaffold a version-pinned provider manifest, schemas, safe fixture, probes, and
125
125
  conformance report;
126
126
  - distinguish structural validation from live protocol negotiation, semantic
127
- admission, and project binding.
127
+ admission, and project binding;
128
+ - keep the agent's source apart from its instance data, with `scripts/fabric_source_check.py check`, which reports a
129
+ home path, a credential-shaped value or a string from the owner's list by path, line and kind and never repeats it
130
+ (FAA-188).
128
131
 
129
132
  The skill never treats generated files as admission. Fabric contract `0.1.0` does not
130
133
  yet ship the host registry/runtime needed to connect and authorize a live provider.
@@ -266,13 +269,21 @@ python3 "$SKILL_DIR/scripts/adapt_project.py" check /path/to/project \
266
269
  ```
267
270
 
268
271
  Pack, sign and verify an agent as a `fabric-package/1` folder (contract DEC-0050, DEC-0051; nothing is written without
269
- `--apply`, and the key file is the person's, never made or kept here):
272
+ `--apply`, and the key file is the person's, never made or kept here). First check the checkout, then make the clean
273
+ copy [the package reference](plugins/fabric-agent-adapter/skills/building-fabric-services/references/package.md)
274
+ describes and check that with `--every-file`, since it is what `pack` carries (a path named in `.fabric-source-ignore`
275
+ or `--exclude` is carried unread; a nested repository is checked in its own tree with `--every-file`):
270
276
 
271
277
  ```bash
272
- npx @passioncode-ai/fabric-agent-adapter pack ./example-agent # the digest, agentRef and every problem
273
- npx @passioncode-ai/fabric-agent-adapter pack ./example-agent --apply
274
- npx @passioncode-ai/fabric-agent-adapter sign ./example-agent --key ./publisher.jwk --apply
275
- npx @passioncode-ai/fabric-agent-adapter verify ./example-agent --trust ./trust.json
278
+ python3 plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_source_check.py check ./example-agent
279
+ python3 plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_source_check.py check ./example-agent-clean --every-file
280
+ ```
281
+
282
+ ```bash
283
+ npx @passioncode-ai/fabric-agent-adapter pack ./example-agent-clean # the clean copy: the digest, agentRef, every problem
284
+ npx @passioncode-ai/fabric-agent-adapter pack ./example-agent-clean --apply
285
+ npx @passioncode-ai/fabric-agent-adapter sign ./example-agent-clean --key ~/.config/example-publisher/publisher.jwk --apply # a key outside any checkout
286
+ npx @passioncode-ai/fabric-agent-adapter verify ./example-agent-clean --trust ./trust.json
276
287
  ```
277
288
 
278
289
  See [the package reference](plugins/fabric-agent-adapter/skills/building-fabric-services/references/package.md).
@@ -285,7 +296,7 @@ Scaffolding is non-destructive by default. It writes only the paths declared in
285
296
  The default pin for new bundles is [`fabric-contract.lock.json`](fabric-contract.lock.json):
286
297
 
287
298
  - version: `0.1.0`
288
- - commit: `00e050ba7429c0c60261cec871779e0fd6aeeb28`
299
+ - commit: `3830a3353923e20f826d466610e1c479f120bec0`
289
300
 
290
301
  Every other mention of the default contract revision (this section, the skills' metadata, the
291
302
  skill card, `adapt_project.py`) must equal it; `python3 test/validate.py` fails a tree
package/SECURITY.md CHANGED
@@ -57,4 +57,9 @@ bounded and non-publishing until explicit scoped grants exist in a project bindi
57
57
  executing it and writes only the seven documented bundle paths (`scaffold --dry-run`
58
58
  writes nothing); `check --contract` runs the contract checkout's own
59
59
  `node_modules/.bin/tsx` and installs nothing. `fabric_provider.py write` writes one
60
- entry (0600) in the providers directory.
60
+ entry (0600) in the providers directory. `fabric_source_check.py check` reads the files of a
61
+ tree (credential values included, in memory only), the owner's list from the user's
62
+ configuration folder, and runs `git ls-files` (without the caller's `GIT_DIR` or other Git
63
+ location variables) and nothing else, except that under a locale whose
64
+ file-system encoding is not UTF-8 it restarts itself once with the same interpreter in Python's
65
+ 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.56.0",
3
+ "version": "0.58.0",
4
4
  "description": "Create Fabric-compatible agents, run them as always-alive local services with dashboards (fabric-service/0.1), and adapt existing projects to the Fabric Agent Contract. This package is the installer CLI.",
5
5
  "scripts": {
6
6
  "test": "python3 test/validate.py && python3 -m unittest discover -s test -q && node --test 'test/*.test.mjs'"
@@ -3,7 +3,7 @@
3
3
  "name": "fabric-agent-adapter",
4
4
  "displayName": "Fabric Agent Adapter",
5
5
  "description": "Create new Fabric-compatible agents, build them as always-alive local services with dashboards (fabric-service/0.1), and adapt existing projects to the Fabric Agent Contract through evidence-backed MCP, A2A, or local-runner provider bundles; and teach a coding agent to work in a project through Fabric (fabric-mcp, fabric_whoami, claim and release), with Claude Code session hooks.",
6
- "version": "0.56.0",
6
+ "version": "0.58.0",
7
7
  "author": {
8
8
  "name": "PassionCode.ai",
9
9
  "url": "https://passioncode.ai/"
@@ -5,9 +5,9 @@ license: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
5
5
  compatibility: Requires filesystem access and Python 3.9+. Exact schema checks additionally need git, Node.js, pnpm, and the pinned fabric-agent-contract checkout (a public repository). Works without those tools in an explicitly degraded structural-check mode.
6
6
  metadata:
7
7
  author: PassionCode.ai
8
- version: "0.56.0"
8
+ version: "0.58.0"
9
9
  contract-version: "0.1.0"
10
- contract-commit: "00e050ba7429c0c60261cec871779e0fd6aeeb28"
10
+ contract-commit: "3830a3353923e20f826d466610e1c479f120bec0"
11
11
  ---
12
12
 
13
13
  # Adapting projects to Fabric
@@ -76,7 +76,7 @@ Use exactly:
76
76
 
77
77
  - contract version `0.1.0`;
78
78
  - repository `https://github.com/passioncode-ai/fabric-agent-contract`;
79
- - commit `00e050ba7429c0c60261cec871779e0fd6aeeb28`.
79
+ - commit `3830a3353923e20f826d466610e1c479f120bec0`.
80
80
 
81
81
  This is the default for new bundles. When checking an issued legacy bundle, preserve its
82
82
  explicit supported lock and use that exact clean checkout. Never silently upgrade or
@@ -159,6 +159,19 @@ A provider entry only describes an MCP server: a CLI that does not speak MCP get
159
159
  announced by its `local-runner` manifest (step 3). An agent that runs as a service uses a
160
160
  descriptor instead (`building-fabric-services`).
161
161
 
162
+ ### 4c. Keep the instance out of the source
163
+
164
+ An agent's source (what its owner could hand over) and its instance (records, logs, keys, the owner's names and paths,
165
+ its configuration) are kept apart; load [the source and the instance](references/source-and-instance.md). First run
166
+ [`scripts/fabric_source_check.py`](scripts/fabric_source_check.py) `check <project-root>`, with the owner's list of
167
+ their own strings or a note that none was given (when the owner's own home name is neutral, has one or two letters,
168
+ or holds a space, `.`, `_`, `-` or `'`, the list gains `home: <name>` first, the reference's rule; a container's, a cloud image's or a CI runner's user that only the image uses, such as `node` or `vscode`, is never declared): it reports instance data by
169
+ path, line and kind, never repeating a value, and changes nothing. Then move each finding out in the source: an
170
+ instance value into the instance configuration outside the repository, read through a named variable with a neutral
171
+ default and a committed `*.example`; a key into the secret store, named only. Run the check again until it exits 0; it is a floor under the owner's review, not a secret scanner.
172
+ A value already committed stays in Git history: say so; a fresh history is the owner's act (no history rewrite, no
173
+ force-push).
174
+
162
175
  ### 5. Make probes safe and meaningful
163
176
 
164
177
  For each capability, create at least one bounded fixture that:
@@ -208,7 +221,8 @@ Update `fabric/FABRIC-CONFORMANCE.md` with dated receipts for:
208
221
  3. bounded semantic probes;
209
222
  4. project-binding readiness.
210
223
 
211
- Include commands and exit codes, immutable endpoint or artifact identities, and explicit
224
+ Include commands and exit codes (a contract checkout named by its commit or `<contract>`, never a home path; run the
225
+ source check again after), immutable endpoint or artifact identities, and explicit
212
226
  unverified surfaces. If live Fabric admission/runtime does not yet exist, the last gates
213
227
  remain `NOT_VERIFIED`; the completed deliverable is an adaptation-ready provider bundle,
214
228
  not a connected provider.
@@ -11,6 +11,7 @@ directory:
11
11
  |---|---|
12
12
  | macOS | `~/Library/Application Support/ai.passioncode.fabric/providers/` |
13
13
  | Linux | `${XDG_DATA_HOME:-~/.local/share}/passioncode-fabric/providers/` |
14
+ | Windows | `%LOCALAPPDATA%\passioncode-fabric\providers\` (the same root as the services folder, DEC-0032; `~\AppData\Local` when `LOCALAPPDATA` is unset) |
14
15
  | any | `FABRIC_PROVIDERS_DIR` when set |
15
16
 
16
17
  A service uses a descriptor instead (the `building-fabric-services` skill). One id is a
@@ -49,15 +50,25 @@ It refuses, with one sentence that never quotes a value:
49
50
  credential itself: a token prefix, a private key, a JWT (FAC-SEM-015: a value is
50
51
  checked by its form; name patterns apply to names);
51
52
  - what the pinned `provider.schema.json` refuses: a URL other than `http://127.0.0.1:<port>/mcp` with a port from 100
52
- to 65535, a manifest path that is not an absolute or `~/` path to `fabric-agent.json` (or is a network share, or over
53
- 1024 characters), a shell-string command, more than 32 arguments or one over 1024 characters, more than 64 env
54
- names, a `source.repository` that is not an absolute URI, an `installedAt` that is not an RFC 3339 date-time,
55
- unknown fields. The patterns are the schema's, copied (`test_provider.SchemaParityTests` compares them with a
56
- contract checkout at the pin when `FABRIC_CONTRACT_NEW` names one);
53
+ to 65535, a manifest path that is not an absolute, `~/` or `~\` path to a file named `fabric-agent.json` after
54
+ either separator (DEC-0145; or is a network share, or over 1024 characters), a shell-string command, more than 32
55
+ arguments or one over 1024 characters, more than 64 env names, a `source.repository` that is not an absolute URI, an
56
+ `installedAt` that is not an RFC 3339 date-time, unknown fields. The patterns are the schema's, copied
57
+ (`test_provider.SchemaParityTests` compares them with a contract checkout at the pin when `FABRIC_CONTRACT_NEW` names
58
+ one, as the contract job of the nightly CI does), and the reader is held to every `provider.schema.json` fixture of
59
+ the pin (`test/fixtures/contract/provider-fixtures.json`, `test_provider.ContractFixtureTests`);
57
60
  - a `source.repository` with a `user:password@` or a value shaped like a credential: it names the repository, never
58
- reaches it (since 0.49.3, as the service kit's descriptor check).
61
+ reaches it (since 0.49.3, as the service kit's descriptor check);
62
+ - a manifest whose name follows a backslash (`C:\Agents\example\fabric-agent.json`). The reader,
63
+ `validate_provider_entry`, accepts it since DEC-0145 (`MANIFEST_PATTERN`, the schema's), but a reader that has not
64
+ re-pinned to DEC-0145 refuses the entry, Fabric's registry among them (CT-192), so the writer does not emit it until
65
+ the oldest Fabric release in use reads it (the release after v0.3.15, CO-479). Write the path with `/` before the name. A manifest under the home folder
66
+ (`~/Agents/example/fabric-agent.json`) is read by every Fabric release; one with a drive letter
67
+ (`C:/Agents/example/fabric-agent.json`) only by Fabric after v0.3.15, whose registry took the repin (CO-479): v0.3.15
68
+ and earlier read only a manifest under `~/` or `/`, so not `~\` either.
59
69
 
60
- `validate` also checks that the file is named `<id>.json` (FAC-SEM-014, slug half).
70
+ `validate` also checks that the file is named `<id>.json` (FAC-SEM-014, slug half): the stem is what follows the last
71
+ `/` or `\`, on every platform (CT-193).
61
72
  From Python: `write_provider_entry(entry)`, `remove_provider_entry(id)`,
62
73
  `validate_provider_entry(entry)`, `manifest_problems(entry)`, `providers_dir()`.
63
74
 
@@ -0,0 +1,154 @@
1
+ # The source and the instance
2
+
3
+ An agent is two things. Its **source** is what can be handed to someone else: code, skills, prompts, schemas,
4
+ examples, the source documentation. Its **instance** is what belongs to one installation: records, logs, journals,
5
+ keys, the owner's names and paths, accounts, hosts and its configuration. A new agent keeps the two apart from its
6
+ first commit, so that its owner can hand the source over later without rewriting its history. Nothing is handed over
7
+ by itself: handing over, a new remote, a push or a change of visibility stays the owner's act, each time (a personal
8
+ agent stays private until its owner says so about that act).
9
+
10
+ The format of a source manifest, export and import is the contract's to decide (CT-194). What follows does not depend
11
+ on it.
12
+
13
+ ## Contents
14
+
15
+ - [The layout](#the-layout)
16
+ - [The check](#the-check)
17
+
18
+ ## The layout
19
+
20
+ | Where | What | In Git |
21
+ |---|---|---|
22
+ | the repository | code, skills, prompts, schemas, the source documentation, fixtures on neutral ids (`example-agent`, `/Users/example`) | yes |
23
+ | `config/<name>.example.json` (or `.env.example`, `*.example.yaml`) | every instance value the agent reads, with a neutral default: the repository address of the owner's account, folders, the owner, account ids, hosts | yes, the example only |
24
+ | the instance folder, outside the repository | the real configuration, records (a database included), logs, journals, state documents | never in the source's repository (a separate private repository of the owner's data belongs to the instance) |
25
+ | the secret store | key values; the source names each key by name only (`EXAMPLE_API_KEY`) | never |
26
+
27
+ - **The instance folder is the agent's data folder.** An agent that is not a kit service reads it through one named
28
+ variable (`<AGENT>_DATA_DIR`) that overrides its default. A service built with `building-fabric-services` takes the
29
+ kit's folders unchanged (the Directories table in its `references/lifecycle.md`, `fabric_paths.service_dirs`): on macOS `~/Library/Application Support/<id>/`,
30
+ on Linux `${XDG_DATA_HOME:-~/.local/share}/<id>/`, on Windows `%LOCALAPPDATA%\<id>\`. An agent's second context
31
+ (an organization's) is a sibling instance, `<id>.<instance>`, never a folder inside the personal one
32
+ (`references/estate-record.md` of that skill).
33
+ - **Ignore the instance in the source**: `.gitignore` names the real configuration (`config/*.json` with
34
+ `!config/*.example.json`), an instance folder that sits inside the checkout during development, and what running
35
+ the agent leaves (`__pycache__/`, `*.pyc`), which the check would otherwise report as binary in Git mode.
36
+ - **State documents are instance data.** A handoff, a decision log or a backlog about this codebase is source
37
+ documentation; a record of what this installation did (runs, conversations, the owner's notes, a journal) lives in
38
+ the instance folder.
39
+ - **Keys by name.** The README's Configure section lists key names, the agent reads each from its environment or the
40
+ secret store, and no value is ever written to a file, a fixture, an argument or a commit.
41
+ - **Fixtures on neutral ids.** A test that needs a home folder, an owner or an account uses `example`, `example-agent`,
42
+ `owner@example.com` or a one-letter home, and a credential-shaped test value says it is synthetic
43
+ (`sk-test-not-a-key`) or is assembled at run time.
44
+ - **Two names stay in the source until the contract decides (CT-194).** A service's supervisor label is the agent's
45
+ own name, reverse-DNS on the publisher's domain (`com.example.example-agent`), and `AGENTS.md` names it (LC-09):
46
+ never the owner's account or name. A provider's `createdBy` names the publisher; until one is chosen it
47
+ keeps the scaffold's sentinel, and a person's name or account is never written there. The adapter moves both when the
48
+ contract decides how an instance renames them (FAA-188 part 2).
49
+
50
+ ## The check
51
+
52
+ `fabric_source_check.py check` reads the files the source would carry (what Git would commit: tracked files and
53
+ untracked ones that are not ignored, as they are in the work tree; a staged copy that differs is not read) and reports each place where the instance shows through, by path, line and kind:
54
+
55
+ | Kind | Finds |
56
+ |---|---|
57
+ | `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) |
58
+ | `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`) |
59
+ | `instance-string` | a string from the owner's list, in a file, in a file's path or in a link's target |
60
+
61
+ ```bash
62
+ python3 <plugin-dir>/skills/adapting-projects-to-fabric/scripts/fabric_source_check.py check <project-root>
63
+ ```
64
+
65
+ - It **never repeats** a value it found: no value, no line holding one, no entry of the owner's list and not the
66
+ folder it checks reaches its output. A file's path is checked like its text (line `0` of a finding), and printed
67
+ with an entry of the list as `<line N>`, a home folder's name as `<home>` and each segment holding a
68
+ credential-shaped value as `<secret>` (two files can then print the same; the segments a found value runs across, an
69
+ assigned value, a bearer run or a URL, are masked too, while a key, a JWT or a JWK `d` is read up to the `/` that
70
+ cuts it, so the rest of a cut one can print in the next folder: find the file by the folder the finding names); an instance string is named by its line in the owner's list. The report can go in a commit
71
+ message, an issue or a chat. A declared home of one or two letters, once found anywhere in the tree, is masked wherever it stands in a printed
72
+ path, inside a word too (`qzone` prints `<home>one`); found nowhere, it is not masked.
73
+ - It **changes nothing.** Exit `0`: no finding and every file read; `1`: at least one finding (the step is not done);
74
+ `2`: the check could not run, or a file was not read: an unreadable file, a nested repository, or a binary file the
75
+ ignore file does not name as source (the summary counts each, and how many files were left out by name). A finding
76
+ sets `1` even when a file was not read too; the files not read are still listed, so fixing the findings can leave `2`.
77
+ - **What it reads.** A text file of any size is read whole, and the summary counts every text file read; a binary file (a NUL byte in its first 8 KiB) or one that
78
+ cannot be read is listed by its path as not read, and the summary counts it. A database or another binary of
79
+ records in the tree is instance data the check cannot read: move it to the instance folder. A binary that is source
80
+ (an icon, a font) is named in the ignore file. `--every-file` reads every file, those Git ignores included, but `.git`, `__pycache__`, `.DS_Store` and `*.pyc`
81
+ (litter `pack` refuses): run it
82
+ on a folder before `pack`, which carries them. A committed symbolic link is checked by the path it names, never followed. When the folder is
83
+ not in a Git checkout, Git lists nothing in it, or no `git` is found in PATH's absolute folders (with PATH unset, in `/bin` and `/usr/bin`; on Windows, nowhere), every file is read but `.git` and `__pycache__` (uncounted) and the dependency
84
+ folders (`node_modules`, virtual environments, caches), and the summary says Git was not used and how many dependency folders were not read.
85
+ Under a locale whose file-system encoding is not UTF-8 (ISO-8859-1, KOI8-R) the check restarts itself in Python's UTF-8
86
+ mode, so a file name, an argument and a file's text are read in one encoding; a name that is not UTF-8 is matched as its bytes.
87
+ A file's text is read as UTF-8, so an entry of the owner's list with a letter outside ASCII is found in a UTF-8 file
88
+ (composed or decomposed, as macOS may store a name), not in one saved as Latin-1 or CP1252.
89
+ In Git mode a nested repository (a submodule, a vendored checkout) is not read: check a nested repository in its own tree with
90
+ the same check (a submodule checked out first, `git submodule update --init`), then name its path in the ignore file,
91
+ with that reason on the line above. That line serves the check of the outer tree only: before `pack`, check the
92
+ nested repository with `--every-file` in its own tree too, because `pack` carries its files, ignored ones included.
93
+ - **The owner's list** holds the strings only the owner knows are theirs (their name, company, accounts, hosts), one
94
+ per line, at least three characters; a line starting with `#` is a comment, so list a channel or a hashtag without its `#`, and a `#` after a string is
95
+ part of that string. An entry matches in any case and inside any word; an invisible format character (a direction mark, a
96
+ soft hyphen, a joiner) at either end of a string is dropped, spaces between such characters too, and does not count
97
+ toward the three; one inside it is kept and counts (an emoji's joiner), so a string copied with a soft hyphen in it
98
+ matches only text that holds one. A line `home: <name>` declares the owner's home
99
+ name instead (ASCII letters, digits, `.`, `_`, `-`, `'` and spaces, starting with a letter or a digit; a name holding a
100
+ space, as a Windows profile may, is written whole, `home: Jane Doe`, and everything after the colon is the name, so
101
+ a note goes on a line of its own (`# laptop`); spaces and tabs may stand around the colon; a line
102
+ that starts with `home` and a colon and holds anything else is refused, exit 2 (an invisible format character before
103
+ the name, after it or inside `home` is dropped, one inside the name refuses the line; a NUL anywhere in the list
104
+ refuses it too); such a line is never a string
105
+ entry, so list a string like `home: page` by a part of it, `ome: page`): the check then finds that home in every form the table names (it is never neutral, even with one or two
106
+ letters), and the bare name is not looked for anywhere else, though a declared name of three or more letters and digits is
107
+ masked wherever a printed path holds it as a word, its parts joined by none or any run of a space, `.`, `_`, `-`, `+`, `'`, their
108
+ 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
109
+ as a plain string too, to be found in text. It is `--instance-strings FILE`, else the file
110
+ `FABRIC_INSTANCE_STRINGS` names, else `passioncode-fabric/instance-strings.txt` in the user's configuration folder
111
+ (`$XDG_CONFIG_HOME` when it is absolute, else `~/.config`; `%APPDATA%` on Windows). A list inside the tree is refused, because it would
112
+ travel with the source. A person who already keeps private terms for org-index's `check_private.py`
113
+ (`~/.config/passioncode/private-terms.txt`) passes that file with `--instance-strings`; this check matches a term
114
+ anywhere in a word, and it does not look for e-mail addresses or account ids by their shape.
115
+ - **Leaving paths out.** `.fabric-source-ignore` at the root (globs, one per line, `#` comments) or `--exclude GLOB`
116
+ leaves out files that hold synthetic values on purpose, such as test vectors. A line is a path from the root, not a
117
+ `.gitignore` line (a leading `/` changes nothing, a line with no `/` matches at the root only, and a line holding `\`, `//`, an unclosed `[` or a class such as `[[:digit:]]` is refused, as Git reads each
118
+ otherwise): `*`, `?` and `[...]` stay inside one folder (`?` and `[...]` match one byte, as Git's do, so a letter
119
+ outside ASCII takes two or more) and `**` as a whole segment crosses
120
+ folders, so `vectors/*` takes the files in `vectors` and none in its subfolders. A line ending in `/`, or a path
121
+ with no wildcard, takes that folder and all it holds; only such a line leaves out a nested repository or a
122
+ submodule (`vendor/sub/`), never a wildcard line without the `/` (`vendor/*`). A wildcard folder line (`vendor/*/`)
123
+ takes every nested repository it reaches, checked or not, so name each one by its path. In Git mode a nested
124
+ 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
125
+ bring back would go unread. Name the files, or a folder that holds
126
+ only synthetic vectors, never a whole source folder (a nested repository checked in its own tree is the one
127
+ exception), and say why on the line above. A file `pack` refuses by its name cannot ship in a package: a public
128
+ certificate is renamed `*.crt`; a project `.npmrc` that holds no credential may be named in the ignore file, with
129
+ that reason, for the source check (the line then hides whatever is added to it later, so keep it credential-free),
130
+ and `pack` still refuses it, so a package is made from a copy without it (its
131
+ settings then apply only where the owner sets them: `~/.npmrc` reaches every project of the owner, and
132
+ `package.json`'s `publishConfig` carries publish-time settings such as `registry`, `tag` and `access`). A file
133
+ holding a credential moves to the instance, never into the ignore file.
134
+ - A neutral home (`example`, `user`, `runner`, a container's, a cloud machine's or a dev container's user such as `node`, `ubuntu` or `vscode`, the fixture id
135
+ `example-agent`, a
136
+ one-letter name, a placeholder; the whole set is `NEUTRAL_HOMES` in the script. An owner whose home name is in it, a
137
+ first name such as `alice` or an `admin` account, or has one or two letters, writes `home: <name>` in the owner's
138
+ list; a container's, a cloud image's or a CI runner's user that only the image uses, such as `node` or `vscode`, is never declared, since
139
+ its paths are the image's and stay in the source; the home of the owner's own machine is declared even when its
140
+ name is an image's user, as `admin` or `ubuntu` on a cloud machine the owner works on; when that name is also the
141
+ user of an image in the tree, the image's paths are reported, and they are written through a build argument that
142
+ names the user, `ARG APP_USER=ubuntu`, `USER ${APP_USER}`, `WORKDIR /home/${APP_USER}/app`, so no home path stands
143
+ in the source; `$HOME` serves only where a shell in the image expands it, a `RUN` line or a script, since a
144
+ Dockerfile's `WORKDIR` and `COPY` see only `ENV` and `ARG` values) and a value that says it is synthetic (`EXAMPLE`, `not-a-key`, `fake`, `dummy`) are
145
+ not findings.
146
+
147
+ The check finds the forms the table names, and only those: a credential in another shape (a bare token with no name
148
+ before it, a provider's key the contract's names do not cover) passes it. It is a floor under the owner's review and a
149
+ secret scanner, never a replacement for either.
150
+
151
+ The check finds the repository from the folder it checks: a Git location the caller exports (`GIT_DIR`,
152
+ `GIT_INDEX_FILE`, as a hook in a linked worktree has) is not passed to Git. The check reads the tree, not Git history. A value that was committed once is still in the history after the tree is
153
+ clean: handing such a source over needs a fresh history, which is the owner's act, never the agent's (no history
154
+ rewrite, no force-push).
@@ -22,10 +22,10 @@ import fabric_secrets as _secrets # noqa: E402 - the contract's credential dete
22
22
 
23
23
  CONTRACT_VERSION = "0.1.0"
24
24
  CONTRACT_REPOSITORY = "https://github.com/passioncode-ai/fabric-agent-contract"
25
- CONTRACT_COMMIT = "00e050ba7429c0c60261cec871779e0fd6aeeb28"
25
+ CONTRACT_COMMIT = "3830a3353923e20f826d466610e1c479f120bec0"
26
26
  # #region supported-contract-revisions — docs: README.md#contract-pin
27
27
  # Only this declaration may name a legacy revision in live files.
28
- SUPPORTED_CONTRACT_COMMITS = (CONTRACT_COMMIT, "1197a90dc4b57152cabcc71ad17db7559f35a27f", "e6c4fc16b415eec3190dd34d924d269405359dc9", "a3a66eb0052043847f6556348e37d34ca873a629", "52c7d798b9f4eb784eb4e29e717038a8912c759d", "2f020a696453ae299b123517f14957f6a1354379", "87d384ffce2757081b7ab227e5dad3dddd08328f", "0a4ce773c4b9e6bec40c6387eb3f988af7152c0e", "e742016dc396aa31e5a13a893489e43f15786c69", "bfa9a67aa3c6cb808c093e7d83cd59799f1ee7d0", "c55f2d5a11c9a0197e958e87212fca7ae1154769", "07042181bfae2a481aa0da1a5651f24c76669c48", "d8146cfb0a1c74fc6432284c4356bb0c864b37b5", "82e163665e156a08aa3b92e4711015582c06162c", "1636fa97262f4a26e2086572d540c2ee701e0010", "f00a7685e3861c27f7899c033fb850a145a1708f", "a046f57cf46ea9e3da57ff8ce6a3ed4d26d8774b", "fe55fc67cba1a9e34a736da7d148d4fe2a8fbfa0", "b291bf09e0a9f9fd3c16cf540832cd021203b7d1", "0cacd08b0e95b6abf131eae1206b60792138bc3c", "2eb91c00e852d9c31d804310d38fad91589fcbac", "8e49fbd5fbbf230e32c1969cd24ac3c22422f0a5", "a83bec6b47776af688402c18efec507143061f68", "52da526d7cc1e063d74c5b520dbc03d0ace15246", "78b2da018d2c4bd9ac1bde1dc3b7f1a110a175f5", "623bf61358c339cb10297807b3f024b5d9f1f327", "23f9fda4c05f8a3852246ee98d4f2adf74ed0875", "9091d3d6606b0c4b5591a38c3674356a01fd7f55", "df55c8c54a23251342a7ee57ba95642b7eb39e61", "2ce392291c6668598d12cd38327e24696b5ca15c")
28
+ SUPPORTED_CONTRACT_COMMITS = (CONTRACT_COMMIT, "00e050ba7429c0c60261cec871779e0fd6aeeb28", "1197a90dc4b57152cabcc71ad17db7559f35a27f", "e6c4fc16b415eec3190dd34d924d269405359dc9", "a3a66eb0052043847f6556348e37d34ca873a629", "52c7d798b9f4eb784eb4e29e717038a8912c759d", "2f020a696453ae299b123517f14957f6a1354379", "87d384ffce2757081b7ab227e5dad3dddd08328f", "0a4ce773c4b9e6bec40c6387eb3f988af7152c0e", "e742016dc396aa31e5a13a893489e43f15786c69", "bfa9a67aa3c6cb808c093e7d83cd59799f1ee7d0", "c55f2d5a11c9a0197e958e87212fca7ae1154769", "07042181bfae2a481aa0da1a5651f24c76669c48", "d8146cfb0a1c74fc6432284c4356bb0c864b37b5", "82e163665e156a08aa3b92e4711015582c06162c", "1636fa97262f4a26e2086572d540c2ee701e0010", "f00a7685e3861c27f7899c033fb850a145a1708f", "a046f57cf46ea9e3da57ff8ce6a3ed4d26d8774b", "fe55fc67cba1a9e34a736da7d148d4fe2a8fbfa0", "b291bf09e0a9f9fd3c16cf540832cd021203b7d1", "0cacd08b0e95b6abf131eae1206b60792138bc3c", "2eb91c00e852d9c31d804310d38fad91589fcbac", "8e49fbd5fbbf230e32c1969cd24ac3c22422f0a5", "a83bec6b47776af688402c18efec507143061f68", "52da526d7cc1e063d74c5b520dbc03d0ace15246", "78b2da018d2c4bd9ac1bde1dc3b7f1a110a175f5", "623bf61358c339cb10297807b3f024b5d9f1f327", "23f9fda4c05f8a3852246ee98d4f2adf74ed0875", "9091d3d6606b0c4b5591a38c3674356a01fd7f55", "df55c8c54a23251342a7ee57ba95642b7eb39e61", "2ce392291c6668598d12cd38327e24696b5ca15c")
29
29
  # #endregion supported-contract-revisions
30
30
  INTEROP_KEY = "https://fabric.passioncode.ai/agent-contract/extensions/interop/0.1"
31
31
  MCP_REVISION = "2026-07-28"
@@ -41,6 +41,9 @@ PROTOCOL = "fabric-provider/0.1"
41
41
  URL_PATTERN = r"^http://127\.0\.0\.1:([1-9][0-9]{2,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])/mcp$"
42
42
  LOCAL_PATH_PATTERN = "^(~[\\\\/]|/(?![\\\\/])|[A-Za-z]:[\\\\/])[^\\u0000]*$"
43
43
  LOCAL_PATH_MAX = 1024
44
+ # DEC-0145 (CT-192): the manifest is named fabric-agent.json after either separator, so a Windows path names it too. The
45
+ # schema's pattern is a search, not a whole-value match; it is applied with its `$` read as \Z (R1, below).
46
+ MANIFEST_PATTERN = r"(^|[\\/])fabric-agent\.json$"
44
47
  ARGV_MAX_ITEMS = 32
45
48
  ARGV_MAX_ITEM = 1024
46
49
  ENV_MAX = 64
@@ -55,6 +58,9 @@ _URI_PASSWORD = re.compile(r"^(?:(?:[Hh][Tt][Tt][Pp][Ss]?|[Ww][Ss][Ss]?|[Ff][Tt]
55
58
  _SECRET_REF = re.compile(r"^secret-ref:([A-Za-z0-9][A-Za-z0-9._/:@-]{0,255})\Z")
56
59
  _LOCAL_PATH = re.compile(LOCAL_PATH_PATTERN)
57
60
  _URL = re.compile(URL_PATTERN)
61
+ _MANIFEST = re.compile(MANIFEST_PATTERN[:-1] + r"\Z")
62
+ # The manifest's name after a backslash: valid since DEC-0145, but no writer emits it until every reader accepts it.
63
+ _MANIFEST_AFTER_BACKSLASH = re.compile(r"\\fabric-agent\.json\Z")
58
64
  # format: uri (RFC 3986 absolute URI): a scheme, a colon, then no whitespace or control character.
59
65
  _URI = re.compile(r"^[A-Za-z][A-Za-z0-9+.-]*:[^\s\x00-\x1f\x7f]+\Z")
60
66
  # format: date-time (RFC 3339).
@@ -89,6 +95,10 @@ def services_dir() -> Path:
89
95
  home = Path.home()
90
96
  if sys.platform == "darwin":
91
97
  return home / "Library/Application Support/ai.passioncode.fabric/services"
98
+ if sys.platform == "win32":
99
+ # provider.md "Where the directory is" (DEC-0032): LOCALAPPDATA, never the roaming APPDATA.
100
+ local = os.environ.get("LOCALAPPDATA")
101
+ return (Path(local) if local else home / "AppData" / "Local") / "passioncode-fabric" / "services"
92
102
  return Path(os.environ.get("XDG_DATA_HOME") or str(home / ".local/share")) / "passioncode-fabric/services"
93
103
 
94
104
 
@@ -128,8 +138,9 @@ def validate_provider_entry(entry: Any) -> List[str]:
128
138
  problems.append("summary is at most 200 characters")
129
139
  manifest = entry["manifest"]
130
140
  if (not isinstance(manifest, str) or len(manifest) > LOCAL_PATH_MAX or not _LOCAL_PATH.fullmatch(manifest)
131
- or not re.search(r"(^|/)fabric-agent\.json\Z", manifest)):
132
- problems.append("manifest is an absolute or ~/ path to fabric-agent.json, at most %d characters, never a network share" % LOCAL_PATH_MAX)
141
+ or not _MANIFEST.search(manifest)):
142
+ problems.append("manifest is an absolute, ~/ or ~\\ path to fabric-agent.json (the name after either separator), at most %d characters, "
143
+ "never a network share" % LOCAL_PATH_MAX)
133
144
  if "source" in entry:
134
145
  source = entry["source"]
135
146
  if not isinstance(source, dict) or not set(source) <= {"repository"}:
@@ -237,8 +248,15 @@ def _service_ids(directory: Path) -> Dict[str, str]:
237
248
  def write_provider_entry(entry: Dict[str, Any], directory: Optional[Path] = None, services: Optional[Path] = None) -> Path:
238
249
  """Installer-only. Refuses an invalid entry and an id that is already a service (FAC-SEM-013)."""
239
250
  problems = validate_provider_entry(entry)
240
- if not problems:
241
- problems = manifest_problems(entry)
251
+ if problems:
252
+ raise ProviderError("Provider entry is invalid: %s." % "; ".join(problems))
253
+ if _MANIFEST_AFTER_BACKSLASH.search(entry["manifest"]):
254
+ # provider.md (DEC-0145): a reader before DEC-0145, Fabric's registry among them, refuses this entry until it
255
+ # re-pins (CT-192), so the writer does not emit it yet.
256
+ raise ProviderError("The manifest's name follows a backslash, which a reader that has not re-pinned to DEC-0145 refuses (CT-192). "
257
+ "Write the path with / before the name: ~/.../fabric-agent.json, which every Fabric release reads, or "
258
+ "C:/.../fabric-agent.json, which Fabric reads only after v0.3.15 (its repin, CO-479).")
259
+ problems = manifest_problems(entry)
242
260
  if problems:
243
261
  raise ProviderError("Provider entry is invalid: %s." % "; ".join(problems))
244
262
  clash = _service_ids(services or services_dir()).get(entry["id"])
@@ -301,7 +319,9 @@ def main(argv: Optional[List[str]] = None) -> int:
301
319
  if args.command == "validate":
302
320
  entry = json.loads(args.path.read_text(encoding="utf-8"))
303
321
  problems = validate_provider_entry(entry)
304
- if isinstance(entry, dict) and entry.get("id") != args.path.stem:
322
+ # CT-193 (FAC-SEM-014): the stem is what follows the last / or \ and loses .json, on every platform.
323
+ stem = re.sub(r"\.json\Z", "", re.sub(r"^.*[\\/]", "", str(args.path), flags=re.S))
324
+ if isinstance(entry, dict) and entry.get("id") != stem:
305
325
  problems.append("the file name %s is not <id>.json for %s (FAC-SEM-014)" % (args.path.name, entry.get("id")))
306
326
  if not problems:
307
327
  problems = manifest_problems(entry)