@passioncode-ai/fabric-agent-adapter 0.69.1 → 0.70.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,27 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.70.0 (2026-11-06)
4
+
5
+ ### Fixed
6
+
7
+ - working-with-fabric: on macOS, with `PASSIONCODE_APPLICATIONS` set to a folder starting with `/`, `status` looks for
8
+ `Fabric.app` only there, as the launcher installs every app in that one folder (its `lib/catalog.js` `appsDir`); an
9
+ empty or relative value keeps the default order (FAA-237, the launcher's reading of its PR #277, board thread
10
+ `cb1df3d5`). `status` names root's copy outside the home "the administrator's copy, which the launcher does not
11
+ update" (`administratorsCopy` and `launcherFolder` in `--json`); a launcher's folder that already holds another
12
+ user's `Fabric.app` is not proposed: `status` says to unset the variable and use `~/Applications`, as the launcher's
13
+ PR #277 advises for a bundle the user cannot write (`bd22566`, open: launcher 0.2.75 with the variable unset
14
+ installs into `/Applications`, so the person puts the copy in `~/Applications`); the label is about the copy
15
+ `status` found, not the registered command, and running it stays allowed. A folder this user cannot enter reads as
16
+ holding no copy: `status` names it in its install advice instead of stopping on Python's `PermissionError` (before
17
+ Python 3.14). With the folder set, `status` names it in its install advice and its `fabric-mcp:` line, never
18
+ `~/Applications` unless that folder holds another user's copy (above). The SKILL.md Step 0 row and the
19
+ `a-second-user-of-a-mac-registers-fabric` eval name the folder.
20
+ - `references/project-board.md` and `references/on-demand-session.md` name Fabric's `board.read-at-start` rule for the
21
+ slot condition of a gate decline: Fabric took it from the adapter into its board rule (main `dbfbfce4`) and into
22
+ `com.enroll`'s own tool description (main `12d57bdd`), from its next release, so a live session gets one instruction
23
+ (FAA-231).
24
+
3
25
  ## 0.69.1 (2026-11-06)
4
26
 
5
27
  The tag `v0.69.0` was never published: its release run
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@passioncode-ai/fabric-agent-adapter",
3
- "version": "0.69.1",
3
+ "version": "0.70.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.69.1",
6
+ "version": "0.70.0",
7
7
  "author": {
8
8
  "name": "PassionCode.ai",
9
9
  "url": "https://passioncode.ai/"
@@ -5,7 +5,7 @@ license: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
5
5
  compatibility: Requires filesystem access and Python 3.9+. Exact schema checks additionally need git, Node.js, pnpm, and the pinned fabric-agent-contract checkout (a public repository). Works without those tools in an explicitly degraded structural-check mode.
6
6
  metadata:
7
7
  author: PassionCode.ai
8
- version: "0.69.1"
8
+ version: "0.70.0"
9
9
  contract-version: "0.1.0"
10
10
  contract-commit: "3830a3353923e20f826d466610e1c479f120bec0"
11
11
  ---
@@ -14,7 +14,7 @@ license: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
14
14
  compatibility: Python 3.9+ or Node.js 20+ for the kits (Node.js 22+ for a Linux service's instance lock); the probe needs Python 3.9+. launchd steps are macOS-only; Linux services install a systemd user unit and Windows ones a per-user Scheduled Task with the kits' writers. Dashboard handoff optionally uses Fabric Dashboards MCP link/host_status/open; without it, report unresolved host capability. The contract checkout is optional.
15
15
  metadata:
16
16
  author: PassionCode.ai
17
- version: "0.69.1"
17
+ version: "0.70.0"
18
18
  contract-version: "0.1.0"
19
19
  extension: "fabric-service/0.1"
20
20
  extension-commit: "3830a3353923e20f826d466610e1c479f120bec0"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fabric-service-kit",
3
- "version": "0.69.1",
3
+ "version": "0.70.0",
4
4
  "about": "The service kit's modules in dependency order: a module imports only modules listed before it. Written by fabric_kit.py build; edit the order and layers by hand, never the digests.",
5
5
  "python": [
6
6
  {
@@ -5496,6 +5496,96 @@
5496
5496
  "fabric-openrouter.mjs": "6041f3ab30f516658e1dbab4897a651cc8509b2136cfa8e769802c6b40ec2f35",
5497
5497
  "fabric-package.schema.json": "2ba799f576fad0f608029223c4ccccd122152f1c5dbfa8ce8b31cd63ac92ad44",
5498
5498
  "share-registration.schema.json": "34b3997eeaf936ebc861342a23045634a7c18c19f01d7ed4198310d81989f5b0"
5499
+ },
5500
+ "0.70.0": {
5501
+ "fabric_jsvalue.py": "052f1b40bab18600b3731dbdca27cbff17588c21d9458fa6241efd1340d0e09a",
5502
+ "fabric_time.py": "588f383da4ac6a243ea95b6ffc3a0aa99ca94245732dc4937734f190b7af4ae9",
5503
+ "fabric_identifiers.py": "0fd814d521ac3aa39907458265870753046a73f48af3d8581ab864b93b211607",
5504
+ "fabric_secrets.py": "53c5df8d6f2c87433c288853bfaad9a43f544789bdf1e39bda8d3ff14485c89d",
5505
+ "fabric_crypto.py": "9c34aee20dbe4d6742e4fdbaab68ceb934bbe7dbd3a904c0010824ace16cdeaa",
5506
+ "fabric_base.py": "f62bcc2ed9912660fa875d27a09f60867260cc4925f79873501fd9620be59611",
5507
+ "fabric_platform.py": "a9350dff55ef0fb3a23cdb4d0320a15868b15a8deb10d2eb74d72c58740c284b",
5508
+ "fabric_paths.py": "8a1ee742f80f436e9f28c61b4f049a9545de2a9e0d66bfc61faa6ee5d1ba3734",
5509
+ "fabric_estate_rules.py": "8b36a7f4108d9c2b1fb38c8bff344778c807bed631a99b4a563ecb0a49c10b1c",
5510
+ "fabric_estate.py": "0737ae1d1bb12586bae49de1f15ef1348bc207b5007cced9dad4c55bb5870874",
5511
+ "fabric_mandate.py": "fb19ce5359606bd6e3352ab02b2a12f49605ec6e5c49b55cce1f79c831325044",
5512
+ "fabric_delegation.py": "716f0ff7ac01095f54b24970beac1962128e8ff33026b243fc0467876142ab03",
5513
+ "fabric_share.py": "da36a466426fc29dd7dd40013167f3959a18375f8fedd228bf64f298cec1040c",
5514
+ "fabric_ceremonies.py": "a59e83cf4adb2669a9b98008e3d3a0cba991a35cbff97bc604fc82fe81cfd49f",
5515
+ "fabric_package_schema.py": "e623f0408d38e3343ed66222fb812907c6c52e1a286eb1c1176ab2e64640c9ad",
5516
+ "fabric_package.py": "1f2b9e73b2b32a7e1e887d4b69f87c6e21b4d1227ad924a235334223e6dd51ca",
5517
+ "fabric_protocols.py": "42529a7df4d753544d2e37a18ec81266d6e1e80477cb8558a3ab521416d45791",
5518
+ "fabric_actions.py": "ae21d1199614c4f74d1dcd1e0a87107a9d52434cd9e2d64aa24a7b1e196fb723",
5519
+ "fabric_connections.py": "7de55cb7d808f756d650effabfaa12027d40a5bd5d2f6d448c32969d47529ecb",
5520
+ "fabric_a2a.py": "8b01502131215bb4de1c01866187c56e99b791543923ed8577535573db33b919",
5521
+ "fabric_page_links.py": "1b57ea573c550e2f90d1ec3edcc655917ceaa0db9dd1635fccf422ec3024d25e",
5522
+ "fabric_lock.py": "dfaf92a7086cbb097d832a305869b7964e2d2a9da0e3365c8ec1685a7bb4fef9",
5523
+ "fabric_usage.py": "5cc2e03ccc5b06e81987ed7bc3eead7a7e6cbf1a7cb8e05c6c64315eed9d91b6",
5524
+ "fabric_backups.py": "1f3f0d7ecf361b29e9101e07f1c53e5dc304d598bf26fd59ec89f16b9fed2420",
5525
+ "fabric_descriptor.py": "c08c91ca0960c694ed7677e7e9247d6307c7b63f37a7e614db022c080cd92ccc",
5526
+ "fabric_context.py": "6ae311ddc7217a4aff915e7ce2646486931acd128462b7bebe4a52bbb5fbb6c7",
5527
+ "fabric_auth.py": "74ccc10239b65a3567cec5213a7b344cbd88d6eee1fd1c5f7be1d6daf985993c",
5528
+ "fabric_events.py": "cd94b186e2a8f8cb4ad2b6bd44d83ec5021933075e28c5d655f387f16dff42b8",
5529
+ "fabric_logs.py": "86dc55bfafa2a57264722e2403510a249b95de0e4a722b2e4bc3948e3bd93da9",
5530
+ "fabric_supervisors.py": "3c5b2e8c310738720b1c1b779be4710e90251013d9be4c68f9952edc0bf55d5f",
5531
+ "fabric_service.py": "4b13f90aa26881fbfe0d9f3077fbb3552f3ce0a3c59279d6baa92df872867d8d",
5532
+ "fabric_profiles.py": "26e21ec29c016000f8ca8053b7b489184854a73be1b7fdc0a18d12990203db9b",
5533
+ "fabric_interop_rules.py": "9bdb7989e57b9d0b0d15eb9982d396499823a5e2c528230b71c67866c736e0f5",
5534
+ "fabric_interop.py": "d27e31834611a37cf2a117746f2f60fc187ccc48718eb546ff923c5d0fce7dea",
5535
+ "fabric_hub.py": "b9ac852e601b1d54cf61351677a628afc69faeed7d1fc74ebe541c1cf38e0c2a",
5536
+ "fabric_board_shapes.py": "0a0e8d3d40e33dfd8c457aedb85330b2ff25f8c49a33eb6cbd5a5cc8a5007f7e",
5537
+ "fabric_board_link.py": "052bc3c474ece835bb428c37ca864810d04e394d991ddb9c93989a7a825073a5",
5538
+ "fabric_board_slots.py": "92b065e3df935547cf46653245f1129e4a1f8b59b0a41e8fa9ffdc41596ee592",
5539
+ "fabric_board_session.py": "1fd3c3884f3044b56a40f78b33ff88dcac9272284f6681ea359c662879826f7e",
5540
+ "fabric_board_fixture.py": "69761ce205ec640762ec9b591c6903e5a87f844e8d423b041681924027e39d1e",
5541
+ "fabric_board.py": "f86c40b8291f8c818f6924910f4022b013d5fd107abaf8825dd0aa6c5e418638",
5542
+ "fabric_http.py": "8b443477dc29d5a1fe908b9dccaf68f05fab837eae0670074051a1dd2dfb71ad",
5543
+ "fabric_openrouter.py": "d06a40f83f2fb8bd441ab3485ae882f1fbdd9cd7a9c1ec175eb47cb8ae2cce81",
5544
+ "fabric-jsvalue.mjs": "dfbdb8c41584972c9389750a41719374a1cfc5652ed438dd1623025190b0106e",
5545
+ "fabric-time.mjs": "3ff932069038a720df1c68c6492976906737b2e67e5c12d9c81c418597f661de",
5546
+ "fabric-identifiers.mjs": "a18c2a501a4aa170bfd3d99e571ad6b7662649b4d3b9d2a540c002ea42811bbf",
5547
+ "fabric-secrets.mjs": "5822a7e25ecf2e87b5bab4f99c1074af7379d8b49e2784283b5d7d8c25022cde",
5548
+ "fabric-crypto.mjs": "038e03689701e3c9721a1e7f8e0a4d02a52d6a5d1661b0dc84035edc3ebbfcf6",
5549
+ "fabric-base.mjs": "7804b2df7715dbbf9702a9b61b6b8cef2110f9a52b04cabd8fb0a66458d87512",
5550
+ "fabric-platform.mjs": "a8119d95ebba4d589a9e401f5d6fc8dda62d59d44e4c1ff811d97fac3771ac0c",
5551
+ "fabric-paths.mjs": "aa630d611e748a899e7043480f4a4e46b444f2e15ff945f30c2fbef21c1ceea5",
5552
+ "fabric-estate-rules.mjs": "7476be71174aa5b1026134f2b335abf647feaa55584e2f9de8618e3054aa8e1e",
5553
+ "fabric-estate.mjs": "e8a2e62d2313121e6cb502f6e6abad94d1d546a44b4d9b3ce2be29e7808e8cf3",
5554
+ "fabric-mandate.mjs": "b70f3de03a659e60bba7b5a0580b3582825cd7b8a8e30a28c79621919ec5b7bb",
5555
+ "fabric-delegation.mjs": "8f63a63aab9a8746b7f664720b87e9398137d01b8d50880974d5070038e414a0",
5556
+ "fabric-share.mjs": "bfb26581c21ad984c2a337da02429f9f3a165cc582f8ba48de4d3dfb7d163a59",
5557
+ "fabric-ceremonies.mjs": "117d4374c9e95c930a38de7adbd4323774798f4fe6ec6f24afcd6f2c7298ca44",
5558
+ "fabric-package-schema.mjs": "cd28092375db0fc21ffab1f0bb7794f7f8979ee08c64d3b9faf4b3845357caa6",
5559
+ "fabric-package.mjs": "8cfb32eebbd4e1bcf28a6e4437344ae2dd54479b0e7c820f002e9f94ab9ecaad",
5560
+ "fabric-protocols.mjs": "5fd8765fdd3e507cc85c108492682bf5cbb4ae08873245c50374fb01d33c75e4",
5561
+ "fabric-actions.mjs": "06e42f5e24c96311d5417d0d1effa93479ef7128a7ab139903b2bcf59253f6be",
5562
+ "fabric-connections.mjs": "303432e547ce563e90ca9ffcd827f5612f51d3582929834e1add0fdaf6485dcc",
5563
+ "fabric-a2a.mjs": "51b72fe1d80649d659fcad3a8f96370db3476c4a7b30cbabd8522c138be69346",
5564
+ "fabric-page-links.mjs": "a8834926e9d4bbf7c4857f551451d2ebf6ac8873bbccaaf89c5e30cf45b17ee6",
5565
+ "fabric-lock.mjs": "02cf833c97a07a8b3c788f9c93f70c4b9c9066efa25318dccf0038e0fc79717f",
5566
+ "fabric-usage.mjs": "1c4d18004639409624ac3516a02f2b60bbf55abdb612e9a96c37e83666eb87a7",
5567
+ "fabric-backups.mjs": "cfd8c9085e3d597a783099c2bd2ffe2f3faa0daaadab46dedd30523894cfeb06",
5568
+ "fabric-descriptor.mjs": "7c0ef7d245c037eb9e859ab481f6e66fb3e5ef2cf6d1e0a8d32b6d303097ca43",
5569
+ "fabric-context.mjs": "1112d695e872f9825cbc94f08484974bff3880087bc29b8be59f885f03e8f7be",
5570
+ "fabric-auth.mjs": "10ff37a39f9e3adb4d0f05db66c45f49b10eef582e9220c213907964109d7e52",
5571
+ "fabric-events.mjs": "c463d6ffcae24229a5c608d8fcea4268d2ccc5024b19fa10e5a452284068fa71",
5572
+ "fabric-logs.mjs": "58467e5719c16e099d05bbedec47a95712506ab03c33f3cd4be1252e8498a946",
5573
+ "fabric-supervisors.mjs": "d45c38de0cb3ed85e083496c2d3c51dab004454accdd87f4762a512a6d442573",
5574
+ "fabric-service.mjs": "297fef11ac76dc831d12d24c5581b7a93587c300c20664db703b3f924c0f9f1a",
5575
+ "fabric-profiles.mjs": "314376cf2deb87f235fb18b11d3b532e211f585b9af0a9302b5f4cc39971b532",
5576
+ "fabric-interop-rules.mjs": "b4e386c359a48e49ffff8a842215760c18a7c98a0af4754a808a35fb2820d458",
5577
+ "fabric-interop.mjs": "989040aed33b2712a258244192bdf70e811a51076aa1b62f2adc2a6db051dbed",
5578
+ "fabric-hub.mjs": "c005115a864cfd67daa4862097bb14acfe5a60bf653897e5bf8a570dd80f1070",
5579
+ "fabric-board-shapes.mjs": "c1db7b7bd0d58d82bb6d8dfbc3cd1547a256a333052d367e82ff1b3edf2804d7",
5580
+ "fabric-board-link.mjs": "6e7d51f4fd9730d93455f50dd30e97907b8b9e1ca65a1d69f036f2e8ec05cc0d",
5581
+ "fabric-board-slots.mjs": "fc77a3c6e62aa18575568bef4f703cce2edaba8cef648813ebcdbb21bdc27a11",
5582
+ "fabric-board-session.mjs": "18555962e5f6d18080a3e7971c4c6640452542f6042d07917e7ebc073721c415",
5583
+ "fabric-board-fixture.mjs": "83c4b7e33ee47b68385faf2569d38d5c8a0c5cb0f8656090f41e1b0eed2f436e",
5584
+ "fabric-board.mjs": "15fbb4441fcf7d2f285375b75de64ad6cffea129c515084c110245376b07775f",
5585
+ "fabric-http.mjs": "317d7668c291570d4d93bc73295312f2853eb93940f4cf754a4a4619cb87ace9",
5586
+ "fabric-openrouter.mjs": "6041f3ab30f516658e1dbab4897a651cc8509b2136cfa8e769802c6b40ec2f35",
5587
+ "fabric-package.schema.json": "2ba799f576fad0f608029223c4ccccd122152f1c5dbfa8ce8b31cd63ac92ad44",
5588
+ "share-registration.schema.json": "34b3997eeaf936ebc861342a23045634a7c18c19f01d7ed4198310d81989f5b0"
5499
5589
  }
5500
5590
  },
5501
5591
  "data": {
@@ -5,7 +5,7 @@ license: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
5
5
  compatibility: Requires filesystem access and Python 3.9+. Exact schema checks additionally need git, Node.js, pnpm, and the pinned fabric-agent-contract checkout (a public repository). Works without those tools in an explicitly degraded structural-check mode. Ships in one plugin with adapting-projects-to-fabric, whose scripts it reuses.
6
6
  metadata:
7
7
  author: PassionCode.ai
8
- version: "0.69.1"
8
+ version: "0.70.0"
9
9
  contract-version: "0.1.0"
10
10
  contract-commit: "3830a3353923e20f826d466610e1c479f120bec0"
11
11
  ---
@@ -18,7 +18,7 @@ compatibility: >-
18
18
  this plugin; Codex/Kimi use command hooks; Hermes/OpenCode/OpenClaw/Pi use native context plugins after opt-in.
19
19
  metadata:
20
20
  author: PassionCode.ai
21
- version: "0.69.1"
21
+ version: "0.70.0"
22
22
  ---
23
23
 
24
24
  # Working with Fabric
@@ -50,7 +50,7 @@ By hand, when Python is absent: Fabric publishes `hub.json` in its folder while
50
50
 
51
51
  | OS | Fabric's folder (`hub.json`) | `<Fabric>/bin` |
52
52
  |---|---|---|
53
- | macOS | `~/Library/Application Support/ai.passioncode.fabric/` | `~/Applications/Fabric.app/Contents/Resources/bin`, then `/Applications` if yours or root's |
53
+ | macOS | `~/Library/Application Support/ai.passioncode.fabric/` | `$PASSIONCODE_APPLICATIONS/Fabric.app/...` if absolute, else `~/Applications/Fabric.app/Contents/Resources/bin`, then `/Applications` if yours or root's |
54
54
  | Windows | `%LOCALAPPDATA%\passioncode-fabric\` | the install folder's `resources\bin` (`fabric-mcp.cmd`); per-user default `%LOCALAPPDATA%\Programs\Fabric` |
55
55
  | Linux | `$XDG_DATA_HOME/passioncode-fabric/` (default `~/.local/share/`) | the install folder's `resources/bin`; the `.deb` puts it in `/opt/Fabric` |
56
56
 
@@ -88,7 +88,7 @@ claude mcp add --scope user fabric -- "<Fabric>/bin/fabric-mcp" --runner claude-
88
88
  codex mcp add fabric -- "<Fabric>/bin/fabric-mcp" --runner codex
89
89
  ```
90
90
 
91
- On a Mac, a registration naming another user's copy (outside your home, neither yours nor root's) is replaced by your
91
+ On a Mac, a registration naming another user's copy (outside your home, not yours or root's) is replaced by your
92
92
  own (or root's) copy: `claude mcp remove` then `add` with the same scope; a project's `.mcp.json` entry is overridden with
93
93
  `--scope local`, never edited (`status` checks Claude Code's).
94
94
 
@@ -154,7 +154,7 @@ Fabric v0.3.5+ (REQ-38):
154
154
  nothing either (Fabric chose that parent, FAA-153) and tell the person which. `spawned_from: null` comes
155
155
  with `spawned_from_why`: link it yourself once with `fabric_task_link {taskId: <the piece>, rel: "spawned",
156
156
  targetTaskId: <the claimed task>}`, and if that is refused too, tell the person that reason; never file the piece
157
- again. A filed piece (`created: true`) with no `spawned_from` at all is a Fabric before 0.3.14: link it yourself the same way unless the answer says
157
+ again. A filed piece (`created: true`) with no `spawned_from` is a Fabric before 0.3.14: link it yourself the same way unless the answer says
158
158
  `chained: true` with `chained_to` your claimed task (board `c42071bc`); an answer with `chained: false` names its
159
159
  `chain_reason`, which you tell the person if your link is refused.
160
160
  3. A related-work warning (same backlog row, a linked task, an overlapping folder) names the other agent, its
@@ -340,7 +340,7 @@ python3 <skill-dir>/scripts/fabric_session.py install-host-hooks --host codex --
340
340
  It edits `~/.codex/hooks.json` (`hooks.SessionStart`) or appends one `[[hooks]]` table to `~/.kimi-code/config.toml`,
341
341
  backs the file up first, refuses a file of another shape or one that changed since the dry run, and `--remove` takes
342
342
  it out again. Hermes/OpenCode/OpenClaw/Pi use their native plugin loaders, not Claude hook JSON.
343
- Hermes also needs `hermes plugins enable fabric-agent-adapter`; removal uses `hermes plugins disable fabric-agent-adapter`.
343
+ Hermes needs `hermes plugins enable fabric-agent-adapter`; removal uses `hermes plugins disable fabric-agent-adapter`.
344
344
  Read [native host hooks](references/native-host-hooks.md) for exact paths, permission behavior and verification.
345
345
  Registration and a loaded hook do not prove admission. Never edit those files by hand. Without the hook, put the preamble into the first prompt
346
346
  yourself: "Call `fabric_whoami` first; `com.status` and `com.list`, answering what waits this turn; `fabric_work_list`,
@@ -114,10 +114,12 @@ hook, or a Switchboard before 0.7.8 started the run, the launcher's appended exi
114
114
  where CO-506 grants nothing (a machine marked as an organization's, no live credential for the session or its runner,
115
115
  or a standing denial of that ask); the reply then names CO-506 though it shipped, and the real gate is that grant (left open in FAA-215).
116
116
  FAA-219; Fabric's answer in board thread `1add3c03` (option (b), 2026-11-05), the responder's run's rule plus the
117
- slot condition, which is the adapter's (Fabric's corrected description has none);
117
+ slot condition, which Fabric's `board.read-at-start` took from the adapter (FAA-231, main `dbfbfce4`, in no release
118
+ yet);
118
119
  Fabric's `com.enroll` description in 0.3.16 tells a session to ask the grant, which it cannot, and 0.3.16's own
119
- `not_authorized` refusal says to reply with the answer; step 3 wins over both, and over the corrected description on Fabric's
120
- `main` (which answers whatever the slot reads); the refusal text waits for its CO-507. An earlier run may have left the request taken: `com.get` on the
120
+ `not_authorized` refusal says to reply with the answer; step 3 wins over both (Fabric's `main` gives `com.enroll`'s
121
+ tool description the same slot condition since `12d57bdd`, FAA-231, in no release yet); the refusal text waits
122
+ for its CO-507. An earlier run may have left the request taken: `com.get` on the
121
123
  request shows its state, and the latest checkpoint (step 1) what that run recorded. An accepted request is never
122
124
  claimed again: take it over with `com.responder_replace {expectedGeneration, transfer}` (its receipt carries the new
123
125
  attempt). An `outcome_unknown` one is resolved only by `com.reconcile` with an observed result; so is an `in_progress` one an
@@ -195,8 +195,9 @@ operator once with `fabric_question_ask` (`kind: access`, `about: board.gate.<ca
195
195
  `python3 <skill-dir>/scripts/fabric_session.py gate-id <request>` prints, the same a run uses, and a `text` naming the request, the gate and what
196
196
  opens it; without Python, no `commandId` can be made: tell the person only), tells the person (the question stays
197
197
  filed even when that is the operator: his decision is recorded under that `about` in Fabric), and stops handling that
198
- request without waiting (Fabric's `board.read-at-start` rule asks every session the same from its CO-517, main
199
- `603da12f`, in no release yet; the slot condition is this adapter's). Never write "it stays queued" in a reply: the reply itself ends the queue. A
198
+ request without waiting (Fabric's `board.read-at-start` rule says the same, slot condition included, from its
199
+ CO-517 and the agreement on FAA-231, main `603da12f` and `dbfbfce4`, and so does `com.enroll`'s own tool description
200
+ since main `12d57bdd`; in no release yet: on Fabric 0.3.16 this rule wins over both). Never write "it stays queued" in a reply: the reply itself ends the queue. A
200
201
  run Fabric started on a board request has no person in its chat to tell: with the slot `absent` at generation 0 it declines,
201
202
  asks for a resend and asks the operator with `fabric_question_ask`; otherwise it leaves the request queued ([on-demand session](on-demand-session.md), step 3).
202
203
 
@@ -48,9 +48,12 @@ from fabric_session_env import ( # noqa: F401
48
48
  _home,
49
49
  fabric_root,
50
50
  state_root,
51
+ a_file,
52
+ administrators_copy,
51
53
  another_users_copy,
52
54
  fabric_bin_candidates,
53
55
  find_fabric_bin,
56
+ launcher_folder,
54
57
  read_small,
55
58
  pid_alive,
56
59
  locate_hub,
@@ -25,11 +25,15 @@ from fabric_session_env import ( # noqa: E402 - the kit (FAA-65)
25
25
  _write_private,
26
26
  claude_registration,
27
27
  codex_registered,
28
+ a_file,
29
+ administrators_copy,
28
30
  another_users_copy,
29
31
  fabric_bin_candidates,
30
32
  fabric_launched,
31
33
  fabric_root,
34
+ APPLICATIONS_ENV,
32
35
  find_fabric_bin,
36
+ launcher_folder,
33
37
  host_registered,
34
38
  launched_by_ancestor,
35
39
  locate_hub,
@@ -722,14 +726,29 @@ def status(cwd: Path, env: Optional[Dict[str, str]] = None, platform: str = sys.
722
726
  foreign = bool(claude) and claude["fabricMcp"] and not command.startswith(("./", ".\\")) and another_users_copy(
723
727
  Path(command), platform, env)
724
728
  report["ownCopyRule"] = platform == "darwin"
729
+ chosen = launcher_folder(env) if platform == "darwin" else None
730
+ report["launcherFolder"] = str(chosen) if chosen else None
731
+ # FAA-237: root's copy runs, and the launcher calls it the administrator's and does not update it
732
+ report["administratorsCopy"] = bool(binary) and administrators_copy(binary, platform, env)
725
733
  report["registeredCopy"] = "another user's" if foreign else "ok"
734
+ # where the user's own copy goes: the launcher's folder when the person named one, else ~/Applications (FAA-237);
735
+ # a launcher's folder that already holds another user's Fabric.app is no place for this user's (0.70.0 seam reading
736
+ # 3): unset PASSIONCODE_APPLICATIONS and use ~/Applications, as the launcher's PR #277 says then (its 0.2.75 would
737
+ # install into /Applications with the variable unset, so the person puts the copy there)
738
+ candidates = fabric_bin_candidates(platform, env)
739
+ occupied = bool(chosen) and a_file(candidates[0]) and another_users_copy(candidates[0], platform, env)
740
+ place = str(chosen) if chosen and not occupied else "~/Applications"
741
+ if occupied:
742
+ unset = {k: v for k, v in (os.environ if env is None else env).items() if k != APPLICATIONS_ENV}
743
+ candidates = fabric_bin_candidates(platform, unset)
744
+ place = "~/Applications, with %s unset: %s holds another user's Fabric.app" % (APPLICATIONS_ENV, chosen)
726
745
  if binary is None and claude is None:
727
746
  report["next"] = "Install Fabric v0.3.5 or later%s; no fabric-mcp %sis at %s." % (
728
- " (your own copy, in ~/Applications)" if platform == "darwin" else "", "of yours " if platform == "darwin" else "",
747
+ " (your own copy, in %s)" % place if platform == "darwin" else "", "of yours " if platform == "darwin" else "",
729
748
  ", ".join(str(p) for p in fabric_bin_candidates(platform, env)))
730
749
  elif foreign:
731
- own = '"%s"' % (binary or fabric_bin_candidates(platform, env)[0])
732
- first = "" if binary else "put your own Fabric.app in ~/Applications, then "
750
+ own = '"%s"' % (binary or candidates[0])
751
+ first = "" if binary else "put your own Fabric.app in %s, then " % place
733
752
  where = claude.get("folder", "")
734
753
  if claude["scope"] == "project":
735
754
  # the project's .mcp.json is shared with every collaborator: a local entry overrides it for this user alone
@@ -760,8 +779,10 @@ def print_status(report: Dict[str, Any]) -> None:
760
779
  "not-a-hub": "not verified: %s" % hub.get("reason")}[hub["state"]]
761
780
  claude = report["claudeCode"]
762
781
  print("Fabric: %s" % line)
763
- print("fabric-mcp: %s" % (report["fabricMcp"] or (
764
- "none of yours at the default install locations" if report.get("ownCopyRule")
782
+ print("fabric-mcp: %s" % ((report["fabricMcp"] + (" (the administrator's copy, which the launcher does not update)"
783
+ if report.get("administratorsCopy") else "")) if report["fabricMcp"] else (
784
+ "none of yours in %s (PASSIONCODE_APPLICATIONS)" % report["launcherFolder"] if report.get("launcherFolder")
785
+ else "none of yours at the default install locations" if report.get("ownCopyRule")
765
786
  else "not found at the default install locations")))
766
787
  print("Claude Code: %s" % ("fabric registered (%s scope) -> %s%s" % (
767
788
  claude["scope"], claude["command"], "" if claude["fabricMcp"] else " (not fabric-mcp)") + (
@@ -58,7 +58,7 @@ SESSION_ID = re.compile(r"[A-Za-z0-9._:-]{1,128}")
58
58
  LOOPBACK_ORIGIN = re.compile(r"http://127\.0\.0\.1:(\d{1,5})")
59
59
 
60
60
 
61
- VERSION = "0.69.1"
61
+ VERSION = "0.70.0"
62
62
 
63
63
 
64
64
  HOSTS = {"claude": "Claude Code", "codex": "Codex", "kimi": "Kimi Code",
@@ -162,11 +162,29 @@ def state_root(platform: str = sys.platform, env: Optional[Dict[str, str]] = Non
162
162
  return Path(env.get("XDG_CACHE_HOME") or str(home / ".cache")) / "fabric-agent-adapter"
163
163
 
164
164
 
165
+ #: The launcher's folder for every app it installs, when the person sets one (FAA-237).
166
+ APPLICATIONS_ENV = "PASSIONCODE_APPLICATIONS"
167
+
168
+
169
+ def launcher_folder(env: Optional[Dict[str, str]] = None) -> Optional[Path]:
170
+ """The folder `PASSIONCODE_APPLICATIONS` names, when it starts with `/` (FAA-237). The launcher's `appsDir` takes
171
+ any non-empty value as it is, so a relative value or a literal `~` would be a folder under the launcher's working
172
+ folder, which no other process can find: none here, and the default order holds. A macOS path is POSIX on
173
+ whatever host reads it."""
174
+ value = (os.environ if env is None else env).get(APPLICATIONS_ENV) or ""
175
+ return Path(value) if value.startswith("/") else None
176
+
177
+
165
178
  def fabric_bin_candidates(platform: str = sys.platform, env: Optional[Dict[str, str]] = None) -> List[Path]:
166
179
  """Where `<Fabric>/bin/fabric-mcp` is on a default install (ADR-0134 amendment 2; electron-builder targets)."""
167
180
  env = os.environ if env is None else env
168
181
  home = _home(env)
169
182
  if platform == "darwin":
183
+ # The launcher's one folder for every app, when the person set it (its lib/catalog.js appsDir; FAA-237): only
184
+ # there. An empty or relative value is not a place, and the default order holds.
185
+ chosen = launcher_folder(env)
186
+ if chosen:
187
+ return [chosen / "Fabric.app/Contents/Resources/bin/fabric-mcp"]
170
188
  # The user's own copy first: on a Mac with several users a copy in /Applications may be another user's
171
189
  # (FAA-229; the launcher's order from its PR #277, lib/catalog.js appPath, planned for launcher 0.2.76).
172
190
  return [home / "Applications/Fabric.app/Contents/Resources/bin/fabric-mcp",
@@ -184,6 +202,15 @@ def _owner_uid(path: Path) -> Optional[int]:
184
202
  return None
185
203
 
186
204
 
205
+ def a_file(path: Path) -> bool:
206
+ """`path.is_file()`, False where it cannot be read: the launcher's folder is the person's choice and may sit under a
207
+ folder this user cannot enter, where Python before 3.14 raises PermissionError (0.70.0 unit reading 4)."""
208
+ try:
209
+ return path.is_file()
210
+ except (OSError, ValueError):
211
+ return False
212
+
213
+
187
214
  def _real(path: Path) -> Path:
188
215
  try:
189
216
  return Path(os.path.realpath(str(path)))
@@ -215,13 +242,24 @@ def another_users_copy(path: Path, platform: str = sys.platform, env: Optional[D
215
242
  return owner is not None and owner not in (os.getuid(), 0)
216
243
 
217
244
 
245
+ def administrators_copy(path: Path, platform: str = sys.platform, env: Optional[Dict[str, str]] = None) -> bool:
246
+ """On macOS, a file outside the user's home that root owns: an install the administrator made (a pkg or MDM). This
247
+ user may run it, and the launcher does not update it (its doctor calls it the administrator's from its PR #277,
248
+ planned for 0.2.76; FAA-237)."""
249
+ if platform != "darwin" or not hasattr(os, "getuid") or os.getuid() == 0 or not path.is_absolute():
250
+ return False
251
+ path = _real(path)
252
+ if _real(_home(os.environ if env is None else env)) in path.parents:
253
+ return False
254
+ return _owner_uid(path) == 0
255
+
256
+
218
257
  def find_fabric_bin(platform: str = sys.platform, env: Optional[Dict[str, str]] = None) -> Optional[Path]:
219
- """The first candidate that is a file. On macOS a copy outside the user's home is taken only when it is this
220
- user's or root's (an install every user shares): another user's copy is skipped (FAA-229). The launcher installs
221
- the user's own copy in `~/Applications` unless the user owns and can write `/Applications` (its lib/catalog.js
222
- appPath, from its PR #277, planned for 0.2.76): it updates the bundle, while this only runs it."""
258
+ """The first candidate that is a file (only the launcher's folder when `PASSIONCODE_APPLICATIONS` names one). On
259
+ macOS a copy outside the user's home is taken only when it is this user's or root's (an install every user
260
+ shares): another user's copy is skipped (FAA-229, FAA-237)."""
223
261
  for candidate in fabric_bin_candidates(platform, env):
224
- if candidate.is_file() and not another_users_copy(candidate, platform, env):
262
+ if a_file(candidate) and not another_users_copy(candidate, platform, env):
225
263
  return candidate
226
264
  return None
227
265