@passioncode-ai/fabric-agent-adapter 0.80.2 → 0.81.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 (76) hide show
  1. package/CHANGELOG.md +79 -0
  2. package/README.md +13 -2
  3. package/SECURITY.md +23 -8
  4. package/bin/fabric-agent-adapter.js +24 -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 +6 -4
  8. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/dashboard-links.md +8 -3
  9. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/source-and-instance.md +44 -7
  10. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/adapt_project.py +107 -2
  11. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/check_dashboard_link.py +14 -2
  12. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_secrets.py +18 -3
  13. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_source_check.py +55 -75
  14. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_source_restart.py +56 -0
  15. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_source_tree.py +912 -0
  16. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/new_agent.py +308 -0
  17. package/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +5 -3
  18. package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/README.md +1 -1
  19. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard-links.md +8 -3
  20. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/delegation.md +5 -2
  21. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/interop.md +28 -0
  22. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/kit-modules.md +3 -2
  23. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/lifecycle.md +25 -9
  24. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/migrating-a-service.md +1 -0
  25. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/package.md +10 -0
  26. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/protocol.md +22 -5
  27. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_dashboard_layout.mjs +21 -0
  28. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_dashboard_link.py +14 -2
  29. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service.py +17 -28
  30. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service_channel_agent.py +2 -1
  31. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service_rules.py +135 -3
  32. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-descriptor.mjs +78 -1
  33. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-holds.mjs +266 -0
  34. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-http.mjs +64 -5
  35. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-package.mjs +98 -1
  36. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-peer-owner.mjs +3 -3
  37. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-protocols.mjs +1 -1
  38. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-secrets.mjs +10 -3
  39. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-service.mjs +5 -0
  40. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-supervisors.mjs +76 -11
  41. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_descriptor.py +85 -1
  42. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_holds.py +316 -0
  43. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_http.py +31 -1
  44. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_package.py +103 -1
  45. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_peer_owner.py +13 -8
  46. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_protocols.py +1 -1
  47. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_secrets.py +18 -3
  48. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_service.py +3 -0
  49. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_supervisors.py +64 -3
  50. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/kit.json +141 -17
  51. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +16 -6
  52. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/agent-templates.md +44 -0
  53. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/communication-agent-template.md +19 -328
  54. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/dashboard-links.md +8 -3
  55. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/models-and-providers.md +1 -1
  56. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/new-agent.md +42 -0
  57. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/source-and-instance.md +44 -7
  58. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/schemas/template/access.schema.json +50 -0
  59. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/schemas/template/connections.schema.json +23 -0
  60. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/schemas/template/data-rooms.schema.json +74 -0
  61. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/schemas/template/manifest.schema.json +170 -0
  62. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/schemas/template/template-header.schema.json +25 -0
  63. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/scripts/check_dashboard_link.py +14 -2
  64. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/scripts/template_check.py +491 -0
  65. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/templates/communication-agent/CHANGELOG.md +7 -0
  66. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/templates/communication-agent/TEMPLATE.md +347 -0
  67. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/templates/communication-agent/access.json +24 -0
  68. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/templates/communication-agent/assembly.md +15 -0
  69. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/templates/communication-agent/checks/acceptance.md +17 -0
  70. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/templates/communication-agent/connections.json +29 -0
  71. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/templates/communication-agent/data-rooms.json +9 -0
  72. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/templates/communication-agent/manifest.json +32 -0
  73. package/plugins/fabric-agent-adapter/skills/observing-products/SKILL.md +1 -1
  74. package/plugins/fabric-agent-adapter/skills/working-with-fabric/SKILL.md +1 -1
  75. package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/project-board.md +1 -1
  76. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_env.py +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,84 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.81.0 (2026-11-12)
4
+
5
+ Wave 3 of the backlog sweep of 2026-11-12. **The contract pin moves to `93e7db16` (`v0.1.0+2026-11-08.1`)**: DEC-0149
6
+ (`fabric-source/1`), CT-254 (HA-2's Linux source, the hybrid the kit already read), DEC-0151, DEC-0152, DEC-0155.
7
+ Bundles pinned at `18f8d487` stay valid (FAA-258; impact map `docs/evidence/specs/2026-11-12-faa-258-repin-93e7db16-impact-map.md`).
8
+ New kit modules: `fabric_holds` and `fabric-holds.mjs`. Kit modules change: `fabric_secrets`, `fabric_protocols`,
9
+ `fabric_peer_owner` (`1.2.0+kit.5`), `fabric_package`, `fabric_descriptor`, `fabric_http`, `fabric_supervisors`, the
10
+ facade `fabric_service`, and their Node twins.
11
+
12
+ ### Added
13
+
14
+ - **`npx @passioncode-ai/fabric-agent-adapter new <folder> --id <agent-id> [--profile ...] [--service]`** (North Star G21,
15
+ FAA-270), and `new_agent.py` behind it: one command creates an agent project with the contract built in: the bundle
16
+ pinned at the contract, `fabric.source.json`, a placement guard reading Observatory's `observatory-placement/1` record
17
+ with its tests, and with `--service` the service kit's sample (its modules copied by `fabric_kit.py install`).
18
+ Idempotent, refuses a folder holding other files without `--force`, JSON last. It needs Python 3.9 or later on the
19
+ machine. Turning a sentence into an agent (`new "<sentence>"`, `test`) stays FAA-26, waiting on Fabric's
20
+ `fabric.agent.create`.
21
+ - **`fabric.source.json` (DEC-0149, `fabric-source/1`)** (FAA-261): `fabric_source_tree.py` ports the contract's source
22
+ rules and matches all 140 of its vendored vectors; `fabric_source_check.py check` reads the source a manifest names,
23
+ by path, line and column, never the value; `adapt_project.py source` lays the manifest down with the kit's slots
24
+ (`PUBLISHER_URI` behind the `createdBy` sentinel, `PLACEMENT_RECORD`, and for a service `SUPERVISOR_LABEL` and
25
+ `SERVICE_PORT`; FAA-188's part 2); `pack` writes `secrets[]` and `state[]` from the slots, and FAC-SEM-160 refuses a
26
+ package that differs.
27
+ - **Work held by a service's own limits** (FAA-298): new kit modules `fabric_holds` and `fabric-holds.mjs` (L3). A held
28
+ job says why it waits in `statusMessage` with the contract's pause grammar (the job schema is closed); the operator gets
29
+ one push per hold, and the answer is recorded as a scoped lift that falls back when its window ends; a new probe rule
30
+ `limits.holds`.
31
+ - **Agent template folders** (FAA-265): five JSON Schemas and `template_check.py` in creating-fabric-agents; the
32
+ communication agent template moved into `templates/communication-agent/` with its manifest, connections, access, data
33
+ rooms, checks, assembly and changelog; `validate.py` checks every shipped template folder.
34
+
35
+ ### Changed
36
+
37
+ - The probe's `limits.holds` reads a service that declares limits but does not use `fabric_holds` as NOT_RUN with a
38
+ migration note, not FAIL, and counts only code files (a vendored `check_service_rules.py` and docs naming `HoldBook`
39
+ no longer pass it). `LoopbackHTTPServer` and `loopbackServer` read the socket table only for a loopback peer: a peer
40
+ at another address (a `0.0.0.0` bind) is served with no owner under `refuse` and `strict` (the 0.81.0 seam reading).
41
+ - **A kit service refuses a loopback peer of another OS account before it reads the request** (DEC-0154 HA-2 and HA-3,
42
+ FAA-227): `LoopbackHTTPServer` takes `peer_check` and Node's new `loopbackServer(httpServer, {peerCheck})` holds each
43
+ connection paused until the check passes; `refuse` (the default) closes another account's peer and serves the rest,
44
+ `strict` also refuses an owner it cannot tell, `off` reads nothing.
45
+ - `fabric_http` and `fabric-http.mjs` check plain http that reaches loopback by its connected address, `insecure=True` and
46
+ a loopback proxy included (FAA-260). The probe times the well-known answer after the owner check, fails a deeply
47
+ nested body cleanly, compares port claims as ports (`:08080` is `:8080`), names the listener on a failed connect, and
48
+ no longer stops on a well-known body that is not an object; the layout probe opens a loopback page only after its
49
+ listener is this account's (FAA-260).
50
+ - The dashboard link gate opens a raw loopback address in a browser only when `listener_owner` is `this-account` as
51
+ well as the host `not_installed` (FAA-251).
52
+ - LaunchDaemon installs and uninstalls refuse contradicting domains, leftover LaunchAgents, another label's plist and an
53
+ unparseable or binary plist, and remove a plist a failed bootstrap wrote; the probe fails a LaunchDaemon without
54
+ `UserName`, with a `~` command, or loaded in both domains (FAA-286).
55
+ - **The kit's credential detector is the contract's DEC-0151 `SECRET_VALUE`**, in both twins and the three dashboard
56
+ link checkers: a provider prefix counts only in its canonical case and after a boundary; an `sk-` key needs a digit and
57
+ an upper-case letter (16 characters or more) or a digit (32 or more), so `SK-...`, `sk-learn-...` and an `sk-` inside a
58
+ word are no longer keys. Repository names keep their own boundary-free key shapes, as DEC-0151 asks (FAA-258).
59
+ - `fabric_protocols` lists `fabric-source/1`; `fabric_peer_owner` is `1.2.0+kit.5`, one version line with Observatory's
60
+ 1.2.0, the Linux reading unchanged (FAA-258).
61
+ - Every vendored contract vector is regenerated at the new pin with the contract's own generators;
62
+ `make_load_vectors.mjs` reads the pin from the lock file. SECURITY.md, lifecycle.md and protocol.md name DEC-0155 RT-3
63
+ for the vouch; SA-6 (DEC-0152) is the accepted board rule (FAA-258).
64
+
65
+ ### Fixed
66
+
67
+ - `fabric_source_check.py` refuses a copy without `fabric_source_tree.py` with exit 2 and the file's name, not a
68
+ traceback and exit 1; `pack` (both kits) refuses by name a hand-written `secrets[]` naming a secret `fabric.source.json`
69
+ has no slot for, instead of dropping it (the 0.81.0 seam reading). The Windows job runs the loopback peer check.
70
+
71
+ ### Upgrading
72
+
73
+ - A door that gave T0 because `strict` served the peer must also check that the peer's owner is named
74
+ (`peer_owner()` / `connectionOwner`): a peer off loopback has none and is T2. A folder whose `fabric.package.json`
75
+ names secrets its `fabric.source.json` lacks fails `pack` until they are added as `secret` slots.
76
+ - A service that serves loopback HTTP keeps serving its own account's callers; pass `peer_check="strict"` to refuse an
77
+ owner the table cannot tell. A caller of the dashboard link gate passes `listener_owner`. An outside test that runs
78
+ this checkout's probe needs a readable socket table.
79
+ - A service that vendors the kit takes the changed `fabric_secrets`, `fabric_protocols` and `fabric_peer_owner` with
80
+ `fabric_kit.py install`. A string the 0.80 detector called a key may no longer be one (see Changed).
81
+
3
82
  ## 0.80.2 (2026-11-12)
4
83
 
5
84
  0.80.1 was tagged and never published: its `windows` job (run 43212508702) failed one test, so `release` and `publish`
package/README.md CHANGED
@@ -152,7 +152,9 @@ For provider bundles the adapting skill helps an agent author:
152
152
  admission, and project binding;
153
153
  - keep the agent's source apart from its instance data, with `scripts/fabric_source_check.py check`, which reports a
154
154
  home path, a credential-shaped value or a string from the owner's list by path, line and kind and never repeats it
155
- (FAA-188).
155
+ (FAA-188); under the source manifest `fabric.source.json` (contract DEC-0149, `fabric-source/1`, which
156
+ `scripts/adapt_project.py source` proposes) it reads the source the manifest names with the contract's tree check
157
+ (FAA-261), and `pack` writes the package's `secrets[]` and `state[]` from its slots.
156
158
 
157
159
  The skill never treats generated files as admission. Fabric contract `0.1.0` does not
158
160
  yet ship the host registry/runtime needed to connect and authorize a live provider.
@@ -169,6 +171,7 @@ then restart your agents. Alone, without any account:
169
171
  npx @passioncode-ai/fabric-agent-adapter # installs every skill into the agents hub ~/.agents/skills
170
172
  npx @passioncode-ai/fabric-agent-adapter --prune-shadow # remove ~/.claude/skills copies that shadow the plugin
171
173
  npx @passioncode-ai/fabric-agent-adapter --force # update a copy already in the hub (a launcher link is never replaced)
174
+ npx @passioncode-ai/fabric-agent-adapter new ./example-agent --id example-agent [--service] # a new agent project, contract built in
172
175
  ```
173
176
 
174
177
  A copy already in the hub is left alone unless `--force` is given; with it, the new copy is built beside the old one
@@ -300,8 +303,16 @@ python3 "$SKILL_DIR/scripts/adapt_project.py" scaffold /path/to/project \
300
303
  --effect none
301
304
  python3 "$SKILL_DIR/scripts/adapt_project.py" check /path/to/project \
302
305
  --contract /path/to/fabric-agent-contract --json
306
+ python3 "$SKILL_DIR/scripts/adapt_project.py" source /path/to/project --dry-run # propose fabric.source.json
307
+ python3 "$SKILL_DIR/scripts/new_agent.py" ./example-agent --id example-agent # what `npx ... new` runs
303
308
  ```
304
309
 
310
+ `new` (FAA-270) writes a new agent project from the kit's template: the provider bundle pinned to the contract,
311
+ `fabric.source.json`, the placement guard with its tests, and with `--service` the service kit's sample; a repeat
312
+ changes nothing, a folder holding other files is refused without `--force`, and the last line is JSON
313
+ ([new-agent.md](plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/new-agent.md)). Inferring the agent
314
+ from a sentence and running its evals (`new "<sentence>"`, `test`) stay with FAA-26.
315
+
305
316
  Pack, sign and verify an agent as a `fabric-package/1` folder (contract DEC-0050, DEC-0051; nothing is written without
306
317
  `--apply`, and the key file is the person's, never made or kept here). First check the checkout, then make the clean
307
318
  copy [the package reference](plugins/fabric-agent-adapter/skills/building-fabric-services/references/package.md)
@@ -330,7 +341,7 @@ Scaffolding is non-destructive by default. It writes only the paths declared in
330
341
  The default pin for new bundles is [`fabric-contract.lock.json`](fabric-contract.lock.json):
331
342
 
332
343
  - version: `0.1.0`
333
- - commit: `18f8d48762edc5bfb1433657cbfe1c16196df1a6`
344
+ - commit: `93e7db164c7829e2783b4b7705c4113bb5201131`
334
345
 
335
346
  Every other mention of the default contract revision (this section, the skills' metadata, the
336
347
  skill card, `adapt_project.py`) must equal it; `python3 test/validate.py` fails a tree
package/SECURITY.md CHANGED
@@ -31,7 +31,7 @@ bounded and non-publishing until explicit scoped grants exist in a project bindi
31
31
  there with an explicit `domain="gui"`, not refused yet, FAA-286 item 21); the job runs as the account its `UserName`
32
32
  names. The installer accepts the job's answer only on a connection launchd's own job accepted
33
33
  (its pid from `launchctl print system/<label>`) or, where the socket table is restricted, on a port that pid listens
34
- on (`lsof -p`, the weaker vouch below, CT-261). The
34
+ on (`lsof -p`, the weaker vouch below, DEC-0155 RT-3). The
35
35
  token is readable by any process of the same user; the protocol defends against web pages
36
36
  and mistakes, not against hostile local code.
37
37
  - The kits' connectors (`fabric_http`, `fabric_openrouter` and their Node twins) reach the
@@ -48,25 +48,40 @@ bounded and non-publishing until explicit scoped grants exist in a project bindi
48
48
  restricted table, to a port that pid listens on there (the weaker vouch below): a departure from the pinned HA-2 and
49
49
  the contract's later RT-3, accepted in principle by the contract owner as DEC-0161 (proposed; the restricted-table vouch
50
50
  included) and kept as a departure until the next repin (FAA-288). They read, never change:
51
- macOS `sysctl net.inet.tcp.pcblist_n` in the process, or the programs `/usr/sbin/netstat
51
+ macOS `sysctl net.inet.tcp.pcblist_n` in the process (the TCP table in the record layout `netstat -anv` reads;
52
+ the pinned HA-2 table names `netstat -anv` and `pcblist64`, an older layout of the same table), or the programs `/usr/sbin/netstat
52
53
  -anv -p tcp` and `/bin/ps -o uid= -p <pid>` (the Node twin always); Linux
53
54
  `/proc/net/tcp` and `/proc/net/tcp6`; Windows `GetExtendedTcpTable` and the process
54
- token (Python), or `netstat -ano`, `whoami /user` and one PowerShell `GetOwnerSid` query
55
- per new connection (Node). On macOS the Python reader calibrates once per process with a
55
+ token (Python), or `netstat -ano` and one PowerShell `GetOwnerSid` query per new connection,
56
+ with `whoami /user` once a process (Node). On macOS the Python reader calibrates once per process with a
56
57
  short-lived child of its own interpreter listening on a random `127.0.0.1` port; when the
57
58
  table is restricted (macOS 27.2, FAA-267) the launchd installers also run `/usr/sbin/lsof
58
59
  -nP -a -p <pid> -i4TCP -sTCP:LISTEN -Fn` on their own job's pid. That vouch shows the job listens on the port, not that it accepted
59
- the connection (a more specific bind by root over a wildcard job would pass; contract CT-261). A LaunchDaemon's root
60
+ the connection (a more specific bind by root over a wildcard job would pass; the pin's DEC-0155 RT-3 vouches only for exactly `127.0.0.1`, a departure FAA-258 filed). A LaunchDaemon's root
60
61
  installer runs that `lsof` and `ps -o uid=` on its job's pid before it accepts an answer, and reads, never changes, every other
61
62
  plist in `/Library/LaunchDaemons` (to find another holder of its label, `plutil -convert xml1` for a binary one in
62
- Node), `launchctl print system/<label>` and the `UserName`'s uid (`pwd`, or `id -u` in Node). A token-free loopback door is not yet protected against
63
- another account's process (HA-3, FAA-227). An MCP client (Claude Code, Codex) sends the
63
+ Node), `launchctl print system/<label>` and the `UserName`'s uid (`pwd`, or `id -u` in Node). The kit's loopback server reads the table once per accepted
64
+ connection and closes a peer of another account before reading it (HA-3, FAA-227); a token-free door counts as
65
+ protected only under `peer_check="strict"`, which also refuses an owner it cannot tell. An MCP client (Claude Code, Codex) sends the
64
66
  token it was registered with on every later connection without this check (the contract's
65
67
  residue "a coding agent's bearer"): when the port may have changed hands, rotate the token
66
68
  and re-run `mcp-register` (one written as `references/surfaces-and-auth.md` says checks the
67
69
  listener first; the kit ships none). On Linux `/proc` names a socket's creator and
68
70
  no pid, so a listening socket root created and handed to another account (systemd
69
- socket activation with `User=`) reads as root's: a client running as root sends to it.
71
+ socket activation with `User=`) reads as root's: a client running as root sends to it. Such a system unit's
72
+ connection reads as root's until the service accepts it and as the service's account after, so two runs of the
73
+ probe can disagree on it (0.74.0 product reading 10).
74
+ - Credential-free loopback http is checked too (FAA-260 item 16, deliberate): `fabric_http` / `fabric-http.mjs` refuse
75
+ plain http to a loopback listener of another OS account or root (another account's Ollama, a `docker-proxy`) with
76
+ kind `foreign-user` whether or not the request carries a credential, because HA-2 reads nothing such a listener
77
+ answers as the service's, and an answer from another account's process is not this account's data. A caller that
78
+ means to read such a daemon says so with its own connection. With `insecure=True` a URL not spelled as loopback
79
+ (a hosts-file name, `0.0.0.0`, `[::ffff:127.0.0.1]`) and a request through a loopback `http_proxy` are checked by the
80
+ connected peer's address, so the Authorization header never reaches another account's proxy (FAA-260 items 1, 8).
81
+ - The layout probe (`check_dashboard_layout.mjs`) reads a raw loopback page's listener from the socket table just
82
+ before its headless browser opens it, the one-time login URL included, and opens nothing on another account's,
83
+ root's or an untellable listener (FAA-260 items 2, 14). The browser then connects on its own: a listener swapped
84
+ between the read and the browser's connect is a residue, bounded by the login code's 120 s and single use.
70
85
  - The agents database (FAA-275): creating-fabric-agents, adapting-projects-to-fabric, building-fabric-services and
71
86
  working-with-fabric may send board requests to whichever Project the board lists under an `agents-db.*` capability
72
87
  (on an organization's board, any member Project may list it), carrying only what `references/agents-database.md`
@@ -43,6 +43,11 @@ Packages (fabric-package/1):
43
43
  npx @passioncode-ai/fabric-agent-adapter verify <folder> --trust <trust.json> [--countersigned]
44
44
  check the folder and its signatures against the keys you pin
45
45
 
46
+ A new agent (FAA-270; needs Python 3.9 or later):
47
+ npx @passioncode-ai/fabric-agent-adapter new <folder> --id <agent-id> [--profile mcp|a2a|local-runner] [--service] [--force] [--json]
48
+ create an agent project with the contract built in: the provider bundle, fabric.source.json, the placement
49
+ guard and, with --service, the service kit's sample; the last line is JSON
50
+
46
51
  Claude Code (recommended):
47
52
  claude plugin marketplace add ${REPO}
48
53
  claude plugin install ${PLUGIN_ID}
@@ -257,8 +262,26 @@ function main(argv) {
257
262
  return 0;
258
263
  }
259
264
 
265
+ // `new` runs the skill's own script (adapting-projects-to-fabric/scripts/new_agent.py), one implementation for the
266
+ // skill and this CLI, under the first Python 3 found on PATH; its output and exit code pass through unchanged.
267
+ function newAgent(args) {
268
+ const { spawnSync } = require('child_process');
269
+ const script = path.join(ROOT, 'plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/new_agent.py');
270
+ const candidates = process.platform === 'win32' ? [['py', ['-3']], ['python', []], ['python3', []]] : [['python3', []], ['python', []]];
271
+ for (const [command, prefix] of candidates) {
272
+ const probe = spawnSync(command, [...prefix, '-c', 'import sys; sys.exit(0 if sys.version_info >= (3, 9) else 1)'], { stdio: 'ignore' });
273
+ if (probe.status !== 0) continue;
274
+ const run = spawnSync(command, [...prefix, script, ...args], { stdio: 'inherit' });
275
+ return run.status === null ? 1 : run.status;
276
+ }
277
+ console.error('error: `new` needs Python 3.9 or later on PATH (python3, or py -3 on Windows)');
278
+ return 2;
279
+ }
280
+
260
281
  const PACKAGE_COMMANDS = ['pack', 'sign', 'verify'];
261
- if (PACKAGE_COMMANDS.includes(process.argv[2])) {
282
+ if (process.argv[2] === 'new') {
283
+ process.exitCode = newAgent(process.argv.slice(3));
284
+ } else if (PACKAGE_COMMANDS.includes(process.argv[2])) {
262
285
  // The package commands live in the kit (fabric-package.mjs), one implementation for the skill and this CLI.
263
286
  const kit = path.join(ROOT, 'plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-package.mjs');
264
287
  import(require('url').pathToFileURL(kit).href).then((m) => { process.exitCode = m.main(process.argv.slice(2)); });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@passioncode-ai/fabric-agent-adapter",
3
- "version": "0.80.2",
3
+ "version": "0.81.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; and put an agent on a standing watch over a product (observer mode).",
6
- "version": "0.80.2",
6
+ "version": "0.81.0",
7
7
  "author": {
8
8
  "name": "PassionCode.ai",
9
9
  "url": "https://passioncode.ai/"
@@ -9,9 +9,9 @@ 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.80.2"
12
+ version: "0.81.0"
13
13
  contract-version: "0.1.0"
14
- contract-commit: "18f8d48762edc5bfb1433657cbfe1c16196df1a6"
14
+ contract-commit: "93e7db164c7829e2783b4b7705c4113bb5201131"
15
15
  ---
16
16
 
17
17
  # Adapting projects to Fabric
@@ -84,7 +84,7 @@ Use exactly:
84
84
 
85
85
  - contract version `0.1.0`;
86
86
  - repository `https://github.com/passioncode-ai/fabric-agent-contract`;
87
- - commit `18f8d48762edc5bfb1433657cbfe1c16196df1a6`.
87
+ - commit `93e7db164c7829e2783b4b7705c4113bb5201131`.
88
88
 
89
89
  This is the default for new bundles. When checking an issued legacy bundle, preserve its
90
90
  explicit supported lock and use that exact clean checkout. Never silently upgrade or
@@ -184,7 +184,9 @@ path, line and kind, never repeating a value, and changes nothing. Then move eac
184
184
  instance value into the instance configuration outside the repository, read through a named variable with a neutral
185
185
  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.
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
- force-push).
187
+ force-push). Then lay down the source manifest, `fabric.source.json` (DEC-0149): `scripts/adapt_project.py source
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*).
188
190
 
189
191
  ### 5. Make probes safe and meaningful
190
192
 
@@ -65,8 +65,8 @@ host's `open` with `fallback=never`. Never `open <url>`, `open -a Safari`,
65
65
  agent's pages. A browser only when `host_status` confirms `not_installed`, and
66
66
  a raw `http://127.0.0.1:<port>` address only after the listener on that port is
67
67
  this OS account's (DEC-0154 PL-3 and HA-2; the adapter's `building-fabric-services/scripts/fabric_peer_owner.py`: `server_end` of a connection it opens and sends nothing on; `check_service.py <id>.<instance>` prints it as `listener.owner`): a
68
- `foreign-user` listener is reported, never opened (the link gate below does not
69
- read the owner yet, FAA-251). An installed host that failed
68
+ `foreign-user` listener is reported, never opened (the link gate below takes
69
+ that owner as `listener_owner`). An installed host that failed
70
70
  to open is reported, not replaced by HTTP. Links
71
71
  carry no token or login code. Every completion and notification action passes
72
72
  the Fabric Agent Adapter's link gate before delivery: `check_dashboard_link.py`
@@ -119,7 +119,12 @@ Its trusted context is supplied by the host, not by untrusted model output:
119
119
 
120
120
  `native_link` must equal the resolved open_link, with an available host on the
121
121
  viewer's device. `browser_link` must equal http_url, with confirmed not_installed,
122
- if_absent policy and the same device. Contract DEC-0123 PL-3 also allows a browser
122
+ if_absent policy, the same device and `"listener_owner": "this-account"`: the kind
123
+ the host read for the listener on http_url's port (`fabric_peer_owner`'s
124
+ `this-account`, `another-account`, `system`, `unknown`, `none`, or
125
+ `check_service.py`'s `foreign-user`); any other, or none given, refuses the
126
+ browser (DEC-0154 PL-3, FAA-251). The native link does not take it: Fabric
127
+ Dashboards checks its own listener. Contract DEC-0123 PL-3 also allows a browser
123
128
  on `unsupported` (a system the host does not ship for); the gate treats
124
129
  `unsupported` as no fallback, by the operator's rule that a browser opens only on a
125
130
  confirmed `not_installed`, and macOS never reports it (FAA-137). `remote_open` instead contains `device_id`,
@@ -7,13 +7,15 @@ first commit, so that its owner can hand the source over later without rewriting
7
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
8
  agent stays private until its owner says so about that act).
9
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.
10
+ The format of a source manifest, export and import is the contract's: DEC-0149 (`fabric-source/1`, its `source.md`) at
11
+ the pin. The kit lays down `fabric.source.json` ([the source manifest](#the-source-manifest)), and the check reads the
12
+ source under it.
12
13
 
13
14
  ## Contents
14
15
 
15
16
  - [The layout](#the-layout)
16
17
  - [The data home and its kind](#the-data-home-and-its-kind)
18
+ - [The source manifest](#the-source-manifest)
17
19
  - [The check](#the-check)
18
20
 
19
21
  ## The layout
@@ -42,11 +44,11 @@ on it.
42
44
  - **Fixtures on neutral ids.** A test that needs a home folder, an owner or an account uses `example`, `example-agent`,
43
45
  `owner@example.com` or a one-letter home, and a credential-shaped test value says it is synthetic
44
46
  (`sk-test-not-a-key`) or is assembled at run time.
45
- - **Two names stay in the source until the contract decides (CT-194).** A service's supervisor label is the agent's
46
- own name, reverse-DNS on the publisher's domain (`com.example.example-agent`), and `AGENTS.md` names it (LC-09):
47
- never the owner's account or name. A provider's `createdBy` names the publisher; until one is chosen it
48
- keeps the scaffold's sentinel, and a person's name or account is never written there. The adapter moves both when the
49
- contract decides how an instance renames them (FAA-188 part 2).
47
+ - **Two names are slots, never values (DEC-0149, FAA-188 part 2).** A service's supervisor label is the agent's own
48
+ name, reverse-DNS on the publisher's domain (`com.example.example-agent`), and `AGENTS.md` names it (LC-09): never the
49
+ owner's account or name; the label an installation runs under is its `SUPERVISOR_LABEL` slot, with its port in
50
+ `SERVICE_PORT`. A provider's `createdBy` names the publisher; the source keeps the scaffold's sentinel, and the
51
+ publisher's URI is the installation's `PUBLISHER_URI` slot, so a person's name or account is never written there.
50
52
 
51
53
  ## The data home and its kind
52
54
 
@@ -69,6 +71,41 @@ on it.
69
71
  configuration and dependency folders `--every-file` reads too), each exit 0, with the owner's list or its absence recorded, until the contract's export (CT-194)
70
72
  replaces it.
71
73
 
74
+ ## The source manifest
75
+
76
+ `fabric.source.json` at the source's root says what is source and which slots an installation fills, never a value
77
+ (DEC-0149, `fabric-source/1`):
78
+
79
+ | Member | What |
80
+ |---|---|
81
+ | `include` | the source, default deny: a file outside every entry is not source; an entry ending in `/` is a folder, no globs |
82
+ | `exclude` | holes in `include`, read in any letter case and Unicode form (a file an entry names only in another spelling is left out, and the check says `excluded-by-fold`) |
83
+ | `binaries` | the binary files that are source (an icon, a font); another binary blocks |
84
+ | `unchecked` | source paths that travel unread, each with its reason (synthetic test vectors, a licence naming a holder on the owner's list) |
85
+ | `publicAddresses` | the e-mail addresses the source may carry (a support address); another one warns |
86
+ | `slots` | what an installation fills, by name: `secret` (a key), `config` (a setting: a repository URL, a folder, the supervisor label, the publisher's identity), `store` (a data store, with its path in the data folder), `endpoint` (the operator's infrastructure by role) |
87
+
88
+ - **Laying it down.** A new agent gets it from `new_agent.py` (creating-fabric-agents' `references/new-agent.md`); an
89
+ existing project from `adapt_project.py source <project-root>` (adapting-projects-to-fabric's scripts), which proposes `include` from the
90
+ project's top-level entries (what Git would commit, instance data by its name left out) and names the kit's slots
91
+ (`PUBLISHER_URI` where a provider manifest is there, `PLACEMENT_RECORD`, and with `--service` `SUPERVISOR_LABEL` and
92
+ `SERVICE_PORT`); `--secret NAME:PURPOSE` and `--store NAME:PATH:PURPOSE` add the agent's own. `--dry-run` prints the
93
+ proposal and writes nothing; an existing manifest is the owner's and is replaced only with `--force`. The author then
94
+ narrows `include` to what is source.
95
+ - **The check under it.** When the folder it checks holds `fabric.source.json`, `fabric_source_check.py check` reads
96
+ the source the manifest names and reports the contract's kinds (`fabric_source_tree.py`, held to the contract's
97
+ vectors): blocking `home-path`, `credential`, `binary`, `nested-repository`, `instance-string`, `missing`,
98
+ `no-include` and the manifest's own findings (FAC-SEM-156, FAC-SEM-157); warnings `email`, `link-outside`,
99
+ `no-license`, `unused-exclude`, `excluded-by-fold`, each by path, line and column (UTF-16 units), never by the text
100
+ found. Exit 1 on a blocking finding, 0 when only warnings remain and every source file was read, 2 when one was not.
101
+ `.fabric-source-ignore` and `--exclude` are not read there: `exclude` and `unchecked` say it. A declared home
102
+ (`home: <name>`) is still found in every source file.
103
+ - **Packing it.** `pack` (building-fabric-services' `references/package.md`) writes the package's `secrets[]` from the secret slots and `state[]` from the store slots' paths, and refuses a
104
+ package whose members differ (FAC-SEM-160); an empty `state[]` is a copy packed without state, and stays so. A
105
+ hand-written `secrets[]` naming a secret the manifest has no slot for is refused by name, never dropped.
106
+ - **Handing it over** stays the owner's act, each time, on their word for that act (the contract's `source.md`,
107
+ Handing over).
108
+
72
109
  ## The check
73
110
 
74
111
  `fabric_source_check.py check` reads the files the source would carry (what Git would commit: tracked files and
@@ -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 = "18f8d48762edc5bfb1433657cbfe1c16196df1a6"
25
+ CONTRACT_COMMIT = "93e7db164c7829e2783b4b7705c4113bb5201131"
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, "eac1a594aa131851f079da8bb7b2149e1951f35f", "3830a3353923e20f826d466610e1c479f120bec0", "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")
28
+ SUPPORTED_CONTRACT_COMMITS = (CONTRACT_COMMIT, "18f8d48762edc5bfb1433657cbfe1c16196df1a6", "eac1a594aa131851f079da8bb7b2149e1951f35f", "3830a3353923e20f826d466610e1c479f120bec0", "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"
@@ -488,6 +488,98 @@ def scaffold_project(root: Path, args: argparse.Namespace) -> Dict[str, Any]:
488
488
  }
489
489
 
490
490
 
491
+ # #region source-manifest: fabric.source.json (contract DEC-0149, fabric-source/1; FAA-261)
492
+ SOURCE_FILE = "fabric.source.json"
493
+ _SLOT_NAME = re.compile(r"[A-Z][A-Z0-9_]{0,63}\Z")
494
+ #: Instance data never named in include: the repository, credentials by their name, dependency and build folders.
495
+ _NOT_SOURCE = re.compile(r"\.git|\.env(\..*)?|.*\.(pem|key|p12|pfx|jks|keystore|token|credential)|node_modules|\.venv|venv|"
496
+ r"__pycache__|dist|build|\.DS_Store|" + re.escape(SOURCE_FILE), re.IGNORECASE)
497
+ #: The slots the kit names for every agent: the provider's createdBy and, for a service, its supervisor label and port
498
+ #: are instance data (FAA-188 part 2); the placement record is the operator's infrastructure, read at start.
499
+ PUBLISHER_SLOT = {"name": "PUBLISHER_URI", "kind": "config",
500
+ "purpose": "provider.createdBy: the publisher's URI; the source keeps the scaffold's sentinel"}
501
+ PLACEMENT_SLOT = {"name": "PLACEMENT_RECORD", "kind": "endpoint", "optional": True,
502
+ "purpose": "Project Observatory's placement record (observatory-placement/1), read at start"}
503
+ SERVICE_SLOTS = (
504
+ {"name": "SUPERVISOR_LABEL", "kind": "config",
505
+ "purpose": "the launchd label, systemd unit or Scheduled Task this installation runs under (LC-09)"},
506
+ {"name": "SERVICE_PORT", "kind": "config", "purpose": "the loopback port recorded for this OS account (DEC-0154 HA-1)"},
507
+ )
508
+
509
+
510
+ def _parse_slot(text: str, kind: str) -> Dict[str, Any]:
511
+ parts = text.split(":", 2 if kind == "store" else 1)
512
+ if len(parts) != (3 if kind == "store" else 2) or not _SLOT_NAME.match(parts[0]) or not parts[-1].strip():
513
+ raise AdaptationError("--%s takes %s" % (kind, "NAME:PATH:PURPOSE" if kind == "store" else "NAME:PURPOSE"))
514
+ slot = {"name": parts[0], "kind": kind, "purpose": parts[-1].strip()}
515
+ if kind == "store":
516
+ slot["path"] = parts[1]
517
+ if _secrets.secret_value(slot["purpose"]):
518
+ raise AdaptationError("a slot is named, never filled: --%s %s holds a value shaped like a credential" % (kind, parts[0]))
519
+ return slot
520
+
521
+
522
+ def source_manifest(include: Sequence[str], description: Optional[str] = None, secrets: Sequence[str] = (),
523
+ stores: Sequence[str] = (), service: bool = False, provider: bool = True) -> Dict[str, Any]:
524
+ """A fabric-source/1 manifest: include as given (sorted, default deny), the kit's slots, the slots named."""
525
+ manifest: Dict[str, Any] = {"format": "fabric-source/1"}
526
+ if description:
527
+ manifest["description"] = description
528
+ manifest["include"] = sorted(dict.fromkeys(include))
529
+ slots = [_parse_slot(t, "secret") for t in secrets] + [_parse_slot(t, "store") for t in stores]
530
+ slots += ([dict(PUBLISHER_SLOT)] if provider else []) + [dict(PLACEMENT_SLOT)]
531
+ slots += [dict(s) for s in SERVICE_SLOTS] if service else []
532
+ names = [s["name"] for s in slots]
533
+ if len(set(names)) != len(names):
534
+ raise AdaptationError("a slot is named twice")
535
+ manifest["slots"] = slots
536
+ return manifest
537
+
538
+
539
+ def _top_level(root: Path) -> List[str]:
540
+ """The project's top-level entries as include paths: what Git would commit, else what a walk finds; a folder with
541
+ a trailing /. Instance data by its name is left out."""
542
+ names: List[str] = []
543
+ try:
544
+ listed = subprocess.run(["git", "-C", str(root), "ls-files", "-z", "--cached", "--others", "--exclude-standard"],
545
+ capture_output=True, timeout=60)
546
+ if listed.returncode == 0:
547
+ names = [os.fsdecode(n) for n in listed.stdout.split(b"\0") if n]
548
+ except (OSError, subprocess.SubprocessError):
549
+ names = []
550
+ if not names:
551
+ names = [_relative(path, root) for path in _files(root)]
552
+ tops = []
553
+ for name in names:
554
+ head, _, rest = name.partition("/")
555
+ if not _NOT_SOURCE.fullmatch(head):
556
+ tops.append(head + "/" if rest else head)
557
+ return sorted(dict.fromkeys(tops))
558
+
559
+
560
+ def write_source_manifest(root: Path, args: argparse.Namespace) -> Dict[str, Any]:
561
+ """`source`: lay down fabric.source.json from the project's top-level entries. An existing one is the owner's and
562
+ is kept unless --force; --dry-run writes nothing."""
563
+ if not root.is_dir():
564
+ raise AdaptationError("project path is not a directory: %s" % root)
565
+ provider = (root / "fabric-agent.json").is_file()
566
+ manifest = source_manifest(_top_level(root), args.description, args.secret, args.store, args.service, provider)
567
+ target = root / SOURCE_FILE
568
+ exists = os.path.lexists(str(target))
569
+ result = {"project": str(root.resolve()), "manifest": manifest, "exists": exists}
570
+ if args.dry_run:
571
+ result["dryRun"] = True
572
+ return result
573
+ if exists and not args.force:
574
+ raise AdaptationError("%s exists: it is the owner's; --dry-run shows the proposal, --force replaces it" % SOURCE_FILE)
575
+ target.write_text(_json_text(manifest), encoding="utf-8")
576
+ result["written"] = SOURCE_FILE
577
+ result["next"] = ("Review include (default deny), name every key and store as a slot, then run "
578
+ "fabric_source_check.py check on the project (exit 0).")
579
+ return result
580
+ # #endregion
581
+
582
+
491
583
  def _load_json(path: Path, errors: List[str]) -> Optional[Any]:
492
584
  try:
493
585
  return json.loads(path.read_text(encoding="utf-8"))
@@ -834,6 +926,17 @@ def build_parser() -> argparse.ArgumentParser:
834
926
  scaffold.add_argument("--dry-run", action="store_true", help="print the planned writes and a unified diff of each existing path; write nothing")
835
927
  scaffold.add_argument("--json", action="store_true")
836
928
 
929
+ source = sub.add_parser("source", help="lay down fabric.source.json, the agent's source manifest (DEC-0149)")
930
+ source.add_argument("project", type=Path)
931
+ source.add_argument("--description", help="what the agent is, in plain words")
932
+ source.add_argument("--secret", action="append", default=[], metavar="NAME:PURPOSE", help="a key the agent reads by name; repeat")
933
+ source.add_argument("--store", action="append", default=[], metavar="NAME:PATH:PURPOSE",
934
+ help="a data store relative to the data folder (it becomes the package's state[]); repeat")
935
+ source.add_argument("--service", action="store_true", help="a kit service: its supervisor label and port are slots")
936
+ source.add_argument("--force", action="store_true", help="replace an existing fabric.source.json")
937
+ source.add_argument("--dry-run", action="store_true", help="print the proposal; write nothing")
938
+ source.add_argument("--json", action="store_true")
939
+
837
940
  check = sub.add_parser("check", help="check generated structure and optional pinned schema")
838
941
  check.add_argument("project", type=Path)
839
942
  check.add_argument("--contract", type=Path)
@@ -848,6 +951,8 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
848
951
  result = inspect_project(args.project)
849
952
  elif args.command == "scaffold":
850
953
  result = scaffold_project(args.project, args)
954
+ elif args.command == "source":
955
+ result = write_source_manifest(args.project, args)
851
956
  else:
852
957
  result = check_project(args.project, args.contract)
853
958
  if result.get("dryRun") and not args.json:
@@ -28,8 +28,14 @@ CREDENTIAL_PAIRS = {
28
28
  'session': {'token', 'id'},
29
29
  'code': {'login', 'auth', 'access', 'session'}}
30
30
  JWT_SHAPE = re.compile(r'eyJ[A-Za-z0-9_-]{2,}\.[A-Za-z0-9_-]{2,}\.[A-Za-z0-9_-]*')
31
- SECRET_VALUE = re.compile(r'(^bearer\s.+|sk-[A-Za-z0-9_-]{16,}|gh[pousr]_[A-Za-z0-9]{20,}|xox[abposr]-[A-Za-z0-9-]{10,}'
32
- r'|AKIA[0-9A-Z]{16}|glpat-[A-Za-z0-9_-]{20,})', re.I | re.S)
31
+ # The contract's SECRET_VALUE at the pin (DEC-0151): `Bearer ` at a value's start in any case, or a provider key with its
32
+ # prefix in its canonical case after a boundary (the start, a character that is no ASCII letter or digit, or an escape);
33
+ # an `sk-` key holds a digit and an upper-case letter with 16 or more characters, or a digit with 32 or more.
34
+ SECRET_VALUE = re.compile(r'(^[Bb][Ee][Aa][Rr][Ee][Rr]\s.+|(?:^|[^A-Za-z0-9]|%[0-9A-Fa-f]{2}|\\[nrt"\']|\\u[0-9A-Fa-f]{4}'
35
+ r'|\\x[0-9A-Fa-f]{2}|\\[bfv0ae]|\\[0-7]{1,3}|%25[0-9A-Fa-f]{2}|\x1b\[[0-9;:]*m'
36
+ r'|(?:\\u001[bB]|\\x1[bB]|\\e|\\033|\\33)\[[0-9;:]*m)'
37
+ r'(?:sk-(?=[A-Za-z0-9_-]{0,255}?[0-9])(?:(?=[A-Za-z0-9_-]{0,255}?[A-Z])[A-Za-z0-9_-]{16,}|[A-Za-z0-9_-]{32,})'
38
+ r'|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,}))', re.S)
33
39
  LONG_RUN = re.compile(r'[A-Za-z0-9+/=_-]{40,}')
34
40
 
35
41
 
@@ -115,6 +121,11 @@ def check(data):
115
121
  require(all(isinstance(v, str) and 0 < len(v) <= 200 for v in (target, viewer)), 'missing_device')
116
122
  fallback = data.get('fallback', 'if_absent')
117
123
  require(fallback in ('if_absent', 'never'), 'invalid_fallback')
124
+ # DEC-0154 PL-3 and HA-2 (FAA-251): who listens on http_url's port, as the host read it from the socket table
125
+ # (fabric_peer_owner's kinds, or check_service.py's foreign-user). A browser opens a raw loopback address only
126
+ # after that listener is this OS account's; absent, it is unknown.
127
+ owner = data.get('listener_owner', 'unknown')
128
+ require(owner in ('this-account', 'another-account', 'system', 'unknown', 'none', 'foreign-user'), 'invalid_listener_owner')
118
129
  kind = action.get('kind')
119
130
  if kind == 'remote_open':
120
131
  require(set(action) <= {'kind', 'device_id', 'service', 'path'}, 'unexpected_action_field')
@@ -128,6 +139,7 @@ def check(data):
128
139
  require(set(action) == {'kind', 'url'} and action.get('url') == http, 'not_resolved_http_url')
129
140
  require(viewer == target, 'wrong_viewer_device')
130
141
  require(state == 'not_installed' and fallback == 'if_absent', 'browser_fallback_forbidden')
142
+ require(owner == 'this-account', 'listener_not_this_account')
131
143
  else:
132
144
  raise ValueError('unsupported_dashboard_action')
133
145
  return {'ok': True, 'kind': kind}
@@ -30,8 +30,20 @@ CREDENTIAL_PAIRS: Dict[str, tuple] = {
30
30
  JS_SPACE = "\t\n\x0b\x0c\r \xa0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff"
31
31
  _NOT_LINE_END = "[^\n\r\u2028\u2029]" # ECMAScript's `.` without the s flag
32
32
  JWT_SHAPE = re.compile(r"eyJ[A-Za-z0-9_-]{2,}\.[A-Za-z0-9_-]{2,}\.[A-Za-z0-9_-]*")
33
- SECRET_VALUE = re.compile(r"(^bearer[%s]%s+|sk-[A-Za-z0-9_-]{16,}|gh[pousr]_[A-Za-z0-9]{20,}|xox[abposr]-[A-Za-z0-9-]{10,}"
34
- r"|AKIA[0-9A-Z]{16}|glpat-[A-Za-z0-9_-]{20,})" % (JS_SPACE, _NOT_LINE_END), re.IGNORECASE | re.ASCII)
33
+ #: Where a provider key's prefix may start (DEC-0151): the start of a value, a character that is no ASCII letter or
34
+ #: digit, or an escape (a percent-escape, single or double, a backslash escape, an octal, `\\x` with two hex digits,
35
+ #: `\\u` with four, an ESC colour sequence, raw or escaped). A consuming group, as the contract writes it.
36
+ _KEY_BOUNDARY = (r"(?:^|[^A-Za-z0-9]|%[0-9A-Fa-f]{2}|\\[nrt\"']|\\u[0-9A-Fa-f]{4}|\\x[0-9A-Fa-f]{2}|\\[bfv0ae]|\\[0-7]{1,3}"
37
+ r"|%25[0-9A-Fa-f]{2}|\x1b\[[0-9;:]*m|(?:\\u001[bB]|\\x1[bB]|\\e|\\033|\\33)\[[0-9;:]*m)")
38
+ #: The provider keys' shapes, prefixes in their canonical case only (DEC-0151 item 7: `SK-` or `GHP_` is no key). An
39
+ #: `sk-` key holds, within 256 characters, a digit and an upper-case letter with 16 or more characters, or a digit with
40
+ #: 32 or more; the look-aheads are bounded to 255 characters and lazy, so a long run of prefixes reads in linear time.
41
+ _PROVIDER_KEYS = (r"(?:sk-(?=[A-Za-z0-9_-]{0,255}?[0-9])(?:(?=[A-Za-z0-9_-]{0,255}?[A-Z])[A-Za-z0-9_-]{16,}|[A-Za-z0-9_-]{32,})"
42
+ r"|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,})")
43
+ #: 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
44
+ #: boundary. No case-insensitive flag; digits are ASCII. A match includes its boundary character: callers test, never
45
+ #: read the matched text or its index.
46
+ SECRET_VALUE = re.compile("(^[Bb][Ee][Aa][Rr][Ee][Rr][" + JS_SPACE + "]" + _NOT_LINE_END + "+|" + _KEY_BOUNDARY + _PROVIDER_KEYS + ")")
35
47
  _LONG_RUN = re.compile(r"[A-Za-z0-9+/=_-]{40,}")
36
48
  _BEARER_INSIDE = re.compile(r"(?<![A-Za-z0-9_])bearer[%s]+(?=[A-Za-z0-9._~+/-]*[0-9])[A-Za-z0-9._~+/-]{12,}=*" % JS_SPACE,
37
49
  re.IGNORECASE | re.ASCII)
@@ -194,7 +206,10 @@ def secret_url(value: str) -> bool:
194
206
  _REPOSITORY_URI = re.compile(r"\A([A-Za-z][A-Za-z0-9+.-]*:(?://[^/?#]*)?)([^?#]*)(.*)\Z", re.S)
195
207
  _REPOSITORY_NAME = re.compile(r"(?:[a-z0-9._-]+|[A-Za-z._-]+)\Z") # lower case, or letters with no digit (readings 8)
196
208
  _REPOSITORY_AUTHORITY = re.compile(r"[A-Za-z][A-Za-z0-9+.-]*://")
197
- _KEY_AT_A_WORD = re.compile(r"(?:\A|(?<=[._-]))(?:%s)" % SECRET_VALUE.pattern, re.IGNORECASE | re.ASCII)
209
+ #: A repository name's own key shapes, boundary-free and case-sensitive (DEC-0151): SECRET_VALUE takes a consuming
210
+ #: boundary, so composed after a separator look-behind it would miss a key after `-`, `.` or `_`.
211
+ _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,}"
212
+ _KEY_AT_A_WORD = re.compile(r"(?:\A|(?<=[._-]))(?:%s)" % _REPOSITORY_KEYS)
198
213
  _JWT_AT_A_WORD = re.compile(r"(?:\A|(?<=[._-]))%s" % JWT_SHAPE.pattern)
199
214
 
200
215