@passioncode-ai/fabric-agent-adapter 0.55.0 → 0.56.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 +196 -0
- package/README.md +4 -3
- package/SECURITY.md +6 -5
- package/package.json +1 -1
- package/plugins/fabric-agent-adapter/.claude-plugin/plugin.json +1 -1
- package/plugins/fabric-agent-adapter/hooks/hooks.json +1 -1
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/SKILL.md +1 -1
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_provider.py +5 -1
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_secrets.py +73 -18
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +6 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/board.md +70 -20
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/connectors.md +7 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/delegation.md +9 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/interop.md +14 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/kit-modules.md +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/lifecycle.md +1 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/protocol.md +4 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-link.mjs +16 -9
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-session.mjs +87 -156
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-slots.mjs +180 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-descriptor.mjs +4 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-http.mjs +2 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-interop-rules.mjs +15 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-interop.mjs +2 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-openrouter.mjs +12 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-protocols.mjs +12 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_link.py +32 -13
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_session.py +115 -147
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_slots.py +173 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_connections.py +9 -42
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_delegation.py +53 -4
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_descriptor.py +7 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_events.py +4 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_http.py +87 -11
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_interop.py +19 -8
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_interop_rules.py +30 -6
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_openrouter.py +9 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_protocols.py +15 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_secrets.py +73 -18
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_usage.py +1 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/kit.json +134 -24
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/sample_service.py +155 -21
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +1 -1
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/SKILL.md +10 -10
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/project-board.md +2 -1
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/session-hooks.md +39 -13
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session.py +6 -2
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_cli.py +17 -3
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_env.py +23 -6
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_mcp.py +10 -39
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_paths.py +190 -0
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/host_hooks.py +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,201 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.56.0 (2026-10-28)
|
|
4
|
+
|
|
5
|
+
Stability and Windows: the debts of 0.53.0 to 0.55.0 that touch how the hooks, the board responder and the interop
|
|
6
|
+
gate behave.
|
|
7
|
+
|
|
8
|
+
### Fixed
|
|
9
|
+
|
|
10
|
+
- The board responder's notices reach a service's event feed: they were sent at level `warn`, which
|
|
11
|
+
`JsonlEventLog.append` refuses (its levels are `info`, `notice`, `warning`, `error`), so `board.down`,
|
|
12
|
+
`board.access.needed`, `board.slot.moved`, `board.slot.contested`, `board.request.held`, `board.request.skipped`,
|
|
13
|
+
`board.reconcile.needed`, `board.attempt.lost` and `board.outcome.invalid` were dropped wherever `emit=log.append`
|
|
14
|
+
(the reference service included, since 0.28.0; `board.slot.moved` since 0.53.0). They are sent at `warning` now, in
|
|
15
|
+
both twins, as is the new `board.slot.refused` (FAA-163). Each notice names its `subject` (`{type, id}`: its request,
|
|
16
|
+
else its capability, else its episode, such as a block's cause, else the Project), so a host tells one request's,
|
|
17
|
+
capability's or cause's episode from another's. In Python the subject goes to an `emit` whose signature names a
|
|
18
|
+
`subject` parameter with no value bound (`JsonlEventLog.append` now names one); any other `emit` (only `**kwargs`, a
|
|
19
|
+
subject bound with `functools.partial`, a signature that cannot be read) is called as 0.55.0 called it, once, without
|
|
20
|
+
one. The Node responder always passes it in its options object, which an `emit` that does not read it ignores.
|
|
21
|
+
- **working-with-fabric on Windows** (FAA-175, FAA-177). A turn names the repositories it touched on Windows too:
|
|
22
|
+
Git Bash `/c/...` and `/cygdrive/c/...`, `C:/...` and quoted `C:\...` paths, and `git.exe -C` (any case); Bash
|
|
23
|
+
text is split with Git Bash's own quoting, so an unquoted backslash is an escape there too. A `cd` inside `( )`, a
|
|
24
|
+
pipeline's stage or a list sent to the background no longer moves the folder a later relative `-C` is read
|
|
25
|
+
against; a newline ends a command, a here-document's body is text (a `#` comment no longer hides its lines, and
|
|
26
|
+
`<<<` opens none), `&>` only redirects, `git -C /a -C b` runs in `/a/b`, and `pushd` and a `cd` after `{` or `then`
|
|
27
|
+
are read. Cygwin and MSYS Python read paths as POSIX. Before, the Stop hook did not read the
|
|
28
|
+
transcript on Windows at all, and a turn from Windows came without `repositories`. The hooks run `python3`, which
|
|
29
|
+
must name Python 3.9 or later in Git Bash there (session-hooks.md; FAA-183 makes the plugin find it itself).
|
|
30
|
+
- **A relative `git -C` after `cd`** (FAA-166) is read against the folder the command last changed into with an
|
|
31
|
+
absolute `cd`; after a relative or unknown `cd` it is dropped, never read against the session's folder.
|
|
32
|
+
- **The board responder's recovery** (FAA-163, FAA-158). A refused enroll of one capability no longer stops the
|
|
33
|
+
others: it is retried on its own, the pause doubling, and said once as `board.slot.refused`. A slot's move is written
|
|
34
|
+
under the responder's lock and only while its record is unchanged, so an operator's take-back at the same moment
|
|
35
|
+
wins. The slot code moved to its own twin modules, `fabric_board_slots.py` and `fabric-board-slots.mjs`.
|
|
36
|
+
- **An idle stop leaves the board within 3 s** (FAA-173): a responder with no work in hand closes its session with a
|
|
37
|
+
confirmed `DELETE` inside the hard exit (`close(timeout)`: a timeout not above 0 is the session's own, and a session built
|
|
38
|
+
with one takes 15 s, in both twins),
|
|
39
|
+
instead of letting the slot lapse (a board call already in flight when the
|
|
40
|
+
stop comes runs first, and when the join ends before the DELETE the slot lapses as before). The slot records are forgotten only after
|
|
41
|
+
a 2xx: a 404 (the session already gone) keeps them, since a move made before it ended is read at the next
|
|
42
|
+
`com.status`. A refusal of the whole board session (`not_available` included) is the board down, never a refused
|
|
43
|
+
capability; a capability refused, or contested, again after it was held is said again, once per capability. A clean
|
|
44
|
+
leave forgets only the records this session confirmed (a slot it held, paused under the operator's hold included, or
|
|
45
|
+
one `com.status` showed unmoved) and no fenced call has put in doubt since, so a slot moved while the service was
|
|
46
|
+
down stays `moved` when the session is dropped at connect or recovery stops early, and is never enrolled again on
|
|
47
|
+
its own. A paused or retrying slot reads `com.status` before each new ask, so a move during the operator's hold is
|
|
48
|
+
said. `Checkpoint.leave(project, held)` takes `held` as a required argument in both twins.
|
|
49
|
+
- **`action_gate` fails closed** (FAA-139): a spend that is not a finite number is read as no estimate, which a
|
|
50
|
+
mandate's `perCallMaxUsd` ceiling refuses (a mandate with no ceiling covers it; the question is with the contract,
|
|
51
|
+
FAA-185), and a project that cannot be read is never covered by a mandate limited to `projects`. A `null` mandate in
|
|
52
|
+
`_meta` reads as an unreadable one, not as none (FAA-178; Python's `approval_step`, while a Node service passes `{}`
|
|
53
|
+
to `jobStep` itself, interop.md). A job's start (`job_step`) reads spend and project as its answer does, and keeps
|
|
54
|
+
the spend as it read it (a NaN is no estimate, never a bare `NaN` in the job file).
|
|
55
|
+
- **`fabric_secrets` matches the contract's ECMAScript reading** (FAA-136): `\s`, `.` and case folding as JavaScript
|
|
56
|
+
reads them, so both ports flag the same values. `secret_url` also reads a special scheme as `new URL` does: every
|
|
57
|
+
slash or backslash after `https:` (or `http`, `ftp`, `ws`, `wss`) is skipped and a backslash before the query reads
|
|
58
|
+
as a slash, so `https:u:p@host` and `https:\\u:p@host` carry a password in Python as in Node. Python splits the URL
|
|
59
|
+
by hand rather than with `urlsplit`, which refuses a `[` or `]` outside an IPv6 host (`https://u:p[w@h/`), and drops
|
|
60
|
+
one leading `?` from a parameter's name as `URLSearchParams` does. Where `new URL` refuses a host (none, a port over
|
|
61
|
+
65535), Python still reads the password and the parameters, so it can flag more than Node, never less. The
|
|
62
|
+
`source.repository` check of a descriptor and of a provider entry reads a special scheme's password the same way
|
|
63
|
+
(`https:bot:pw@host`), up to the user information's last `@` (`https://me@corp.example:pw@host`), and says so
|
|
64
|
+
before it judges the host. The query and the fragment are read percent-encoded, as `new URL` hands them over.
|
|
65
|
+
`fabric_http.redact_url` reads a special scheme the same way (below).
|
|
66
|
+
- **What the kit sends, refuses and redacts as a URL** (FAA-136; the remaining differences are FAA-187):
|
|
67
|
+
- `redact_url`: the URL an `HttpError` carries for `https:u:pw@host` has no password, as in Node; a URL of any
|
|
68
|
+
other scheme (a misspelled `htps:` included) keeps only its scheme in both twins (`htps:<redacted>`), since its
|
|
69
|
+
user information may sit anywhere in the rest. It first trims C0 controls and space from the ends and removes
|
|
70
|
+
tabs and line breaks, as `new URL` does, so ` https://u:pw@host` keeps no password on Python 3.9 either. A
|
|
71
|
+
string with no scheme (`ht tps:`, `1ttps:`, a leading byte order mark or no-break space) and a URL whose host or
|
|
72
|
+
port `new URL` refuses (a password holding `/`, a port that is not ASCII digits such as `1_2` or `+1`, which
|
|
73
|
+
Python 3.9's `int()` takes, a bracketed host that is no IPv6 address, a `ws`, `wss` or `ftp` URL with no host or
|
|
74
|
+
a port above 65535) read as `<unparseable url>`; the scheme is read by the WHATWG rule, not by `urlsplit`, whose
|
|
75
|
+
rule differs between 3.9 patch releases. On Python a host holding `%` reads as `<unparseable url>` too, where
|
|
76
|
+
Node writes it decoded. The path is not redacted in either twin (Node writes it as `new URL` normalises it,
|
|
77
|
+
Python as given), so a key never belongs in it.
|
|
78
|
+
- `check_url` and `request`: the same ports and bracketed hosts (an IPv6 zone included) are refused. On Python a
|
|
79
|
+
backslash, `%`, space or control character in the host (a Unicode space and a C1 control included) is refused
|
|
80
|
+
too, since urllib would decode it or dial another host (`h.example%3A8443` reached port 8443), and so is a space
|
|
81
|
+
or ASCII control character before the URL's first `#`, or a character outside ASCII in its path or query, which
|
|
82
|
+
urllib would send and http.client refuse: such a URL is refused before it is sent (kind `refused`), where 0.55.0
|
|
83
|
+
retried it and answered kind `connection`. Python's `check_url` checks the host's shape, not its name (FAA-187
|
|
84
|
+
(14), (16)). The Node twin reads several of these as `new URL` does, among them a tab or line break removed, a
|
|
85
|
+
space or a character outside ASCII percent-encoded, a backslash as `/`, an extra slash before the host skipped
|
|
86
|
+
and a `%` host decoded (refused when the decoded host is not allowed, as `h.example%3A8443`).
|
|
87
|
+
- What is sent: the default transport sends a URL up to its first `#` in both twins (urllib alone kept everything
|
|
88
|
+
before the last one, so `/x#a#b` asked for `/x#a`); a transport you pass receives the URL as given. ASCII
|
|
89
|
+
whitespace at the ends, which urllib and `new URL` both trim, is trimmed, so a URL ending in a line break is still
|
|
90
|
+
sent, as in 0.55.0 (on Python 3.9 a URL with a leading space too, which 0.55.0 refused there). On Python a URL
|
|
91
|
+
whose part before its first `#` ends in a Unicode space (a no-break or ideographic space) is now refused; 0.55.0
|
|
92
|
+
sent it trimmed when the space ended the URL and failed every call when a fragment followed it. Node keeps it
|
|
93
|
+
percent-encoded.
|
|
94
|
+
- The OpenRouter clients trim a base URL's ends first: ASCII whitespace and trailing slashes in both, and on Node
|
|
95
|
+
every C0 control there and a tab or line break anywhere, as `new URL` reads them. So `.../v1\n`, `.../v1/ ` and,
|
|
96
|
+
on Node, `.../v1\n/` are sent to `.../v1/chat/completions`. Both then refuse, when they are built, a base URL
|
|
97
|
+
holding a query or a fragment (kind `config`, read before `check_url`; `/chat/completions` would land in it, so a path-routed endpoint
|
|
98
|
+
such as OpenRouter failed every call in 0.55.0). Python also refuses, through `check_url` (kind `refused`), a
|
|
99
|
+
base with a space, a control or a non-ASCII character in its path, which 0.55.0 accepted and then failed every
|
|
100
|
+
call with; Node sends such a path percent-encoded, as 0.55.0 did (FAA-187 (18)). Attribution answers `not-openrouter` for a port of other
|
|
101
|
+
digits (`:٤٤٣`), as `new URL` reads it.
|
|
102
|
+
- A JWKS source: the Python kit reads its host and port as urllib dials them, https included. It refuses a space
|
|
103
|
+
or ASCII control character before the URL's first `#` (it reads the URL up to there, as Node does), a character
|
|
104
|
+
outside ASCII in its path or query, a host holding `%`, a backslash, a Unicode space or a control character, a
|
|
105
|
+
bracketed host that is no IPv6 address, a port that is not ASCII digits or is above 65535, no host, and any `@`
|
|
106
|
+
(an empty userinfo dialled `@127.0.0.1`). A trailing ASCII space or line break is trimmed, as urllib reads it; a trailing Unicode space
|
|
107
|
+
before the first `#` is refused for the same reason as in `check_url`, though urllib would trim it and read the
|
|
108
|
+
keys when it ends the URL; a leading space is refused as not a URL, as in 0.55.0 and in Node. Neither `[::1%...]` nor
|
|
109
|
+
`http://[::1]%2Eevil.example/` (which Python 3.9's `urlsplit` reads as `::1`) nor a mapped `[::ffff:127.0.0.1]`
|
|
110
|
+
(loopback to Python 3.13+) counts as loopback. The Node kit accepts several of these as `new URL` reads them,
|
|
111
|
+
among them a `%` host decoded, a backslash as `/`, a tab or line break removed, an empty userinfo dropped and an
|
|
112
|
+
extra slash before the host skipped (`https:///j` reads host `j`).
|
|
113
|
+
- **A too-deep protocol value** is refused by name (FAA-122), in both twins.
|
|
114
|
+
- **A job's approval step and its answer read the mandate under the same name** (FAA-179): the job's stored
|
|
115
|
+
capability, also when a handler created the job under another name; `on_fault` hears both under it, not the answer
|
|
116
|
+
under `fabric.job.get`.
|
|
117
|
+
- **Nudges** (FAA-143): `lost=` counts every reminder that did not make it back to the file (this turn's capped lines,
|
|
118
|
+
and the pending ones when the write-back fails too); an answer's nudges are read up to 1,000 items, in linear time.
|
|
119
|
+
|
|
120
|
+
### Added
|
|
121
|
+
|
|
122
|
+
- `FABRIC_SESSION_NO_REPOSITORIES=1`, or a `no-repositories` file in the hooks' folder (which a cache cleaner may
|
|
123
|
+
remove), turns the transcript read off (FAA-160); `fabric_session.py status` says
|
|
124
|
+
whether it is on (only while a running Fabric advertises `session-hooks/1`, `fabric` is registered for the folder and
|
|
125
|
+
Fabric takes the repositories), off, waiting for Fabric, or not done because `fabric` is not registered. The repositories of a turn can miss one (FAA-174): a turn longer
|
|
126
|
+
than the transcript's last 512 KB names only what that tail shows; until FAA-184 they can also name a wrong one,
|
|
127
|
+
since a relative `git -C` in a later Bash call is read against the session's folder.
|
|
128
|
+
- The reference service's dashboard shows a moved slot with a Take back button (`POST /api/board/take-back`, operator
|
|
129
|
+
only; FAA-157), and the button has its tool: `board.take-back`, a `relayed` action an agent whose client sets `_meta`
|
|
130
|
+
calls with the operator's decision (Claude Code and Codex reach the page until Fabric forwards decisions, CT-125).
|
|
131
|
+
The tool is a manifest capability too (effect `change-policy`, idempotency `required`), as every tool the well-known
|
|
132
|
+
lists must be: `register --board-project <uuid>` writes it into the manifest, so `check_service.py` passes the sample
|
|
133
|
+
with its board on. The manifest stays register's to write: a serve whose board mode differs from it at its start
|
|
134
|
+
logs `service.manifest.mismatch` (warning), and the probe fails until it is registered again. Its `/mcp` reads a loopback call the service token authenticated as T0. The
|
|
135
|
+
page, the route and the tool read a move from the checkpoint's record too, so a take-back made after a restart and
|
|
136
|
+
before the session reconnects (or while Fabric is down) is kept; a checkpoint it cannot write is a tool error. A
|
|
137
|
+
repeat after a lost answer is answered `{capability, moved: false}` by the route as by the tool, and each take-back is
|
|
138
|
+
a `board.slot.returned` event (the capability as its subject) whose sentence says the route in words and whose `data`
|
|
139
|
+
holds `by`, `via` and the `caller` (none when the feed refuses the caller), never the decision's words. The service installs its stop handler before its board thread starts, so a SIGTERM at any moment drains and leaves the board session. The
|
|
140
|
+
page's CSP allows the button's own `fetch` (`connect-src 'self'`), and a failed request says so on the button.
|
|
141
|
+
- working-with-fabric Step 4: a `fabric_task_create` that ended in an error may have filed the piece, so look for it
|
|
142
|
+
by title in `fabric_work_list` before filing it again (FAA-168), with the eval `a-failed-create-may-have-filed`.
|
|
143
|
+
|
|
144
|
+
### Changed
|
|
145
|
+
|
|
146
|
+
- working-with-fabric names Fabric v0.3.5 as the release that brought `fabric-mcp` and the `fabric_work_*` tools, not
|
|
147
|
+
0.4.0 (the plan milestone), and goes by whether the tools are listed (FAA-170).
|
|
148
|
+
- The `board.slot.moved` notice says the accepted work stays `accepted` (not "held", the operator's word) and that
|
|
149
|
+
after its take-back action a restart or reconnect while accepted work waits takes the slot back at once; a restart
|
|
150
|
+
alone does not (FAA-164, FAA-171);
|
|
151
|
+
project-board.md says the same to a requester. board.md says a take-back without the operator's relayed decision is
|
|
152
|
+
refused. session-hooks.md gives the Bash cap as 64 K characters and the downgrade window as until a worker next
|
|
153
|
+
reaches the running Fabric.
|
|
154
|
+
|
|
155
|
+
### Upgrading
|
|
156
|
+
|
|
157
|
+
- On Windows the Stop hook now reads the transcript's tail and the touched repositories' `origin` (FAA-175), once
|
|
158
|
+
Fabric takes `repositories`, as it does on macOS and Linux; `FABRIC_SESSION_NO_REPOSITORIES=1` (in Claude Code's
|
|
159
|
+
settings `env`, which reached hooks declared in settings and in a plugin's `hooks.json` in runs on macOS; Windows is
|
|
160
|
+
FAA-183's run) turns it off.
|
|
161
|
+
- A Python `JwksCache` source is now refused when the cache is built, so a service that builds it at start stops
|
|
162
|
+
there, if it is any source the JWKS source item under Fixed lists. Among them, 0.55.0 read: a host holding `%` (an
|
|
163
|
+
IPv6 zone included, as in `http://[::1%25lo0]/`), which urllib decoded and dialled; a port that is not ASCII digits
|
|
164
|
+
(`:1_8080`); a bracketed IPv4 host (`[127.0.0.1]`, which Python 3.9 dialled as `127.0.0.1`); a mapped
|
|
165
|
+
`[::ffff:127.0.0.1]` loopback (on Python 3.13+); a Unicode space ending the URL. Write the host and port plainly
|
|
166
|
+
(`127.0.0.1:18080`) and trim the space. The other sources it lists 0.55.0 accepted and then answered every
|
|
167
|
+
delegated call `issuer-unavailable`.
|
|
168
|
+
- On Python, `check_url` and `request` now refuse before sending (kind `refused`) a URL that 0.55.0 sent: a host
|
|
169
|
+
holding `%` (`h.example%3A8443` reached port 8443), a port that is not ASCII digits (`:1_2` reached port 12 on
|
|
170
|
+
3.9), a bracketed IPv4 host (`[127.0.0.1]`, dialled on 3.9), and a URL whose part before its first `#` ends in a Unicode space (sent trimmed). A URL with a space, a
|
|
171
|
+
control or a non-ASCII character in its path or query, which 0.55.0 retried and answered kind `connection`, is
|
|
172
|
+
refused the same way. Both OpenRouter clients refuse, when they are built, a base URL holding a query or a
|
|
173
|
+
fragment, and the Python client one with a space, a control or a non-ASCII character in its path, so a service
|
|
174
|
+
that builds its client at start stops there. Write the host and port plainly, trim the space, drop the query or fragment where the URL is
|
|
175
|
+
configured, and percent-encode a space, a control or a non-ASCII character in each path segment or query
|
|
176
|
+
value (`urllib.parse.quote(value, safe="")`, `urllib.parse.urlencode` for a query), never the whole URL.
|
|
177
|
+
- A service on the kit joins its responder thread at stop, bounded inside its hard exit, as `sample_service.py` does
|
|
178
|
+
(board.md, "Wiring it into the service"); on Node it aborts the run's signal and awaits the run. The idle leave above
|
|
179
|
+
happens only then. A service that stops without the join still works; its
|
|
180
|
+
slot lapses on the board's clock as before.
|
|
181
|
+
- A service that calls `Checkpoint.leave` itself now passes the generations it confirmed (`held`, required in both
|
|
182
|
+
twins); the responder's own leave does this.
|
|
183
|
+
- A service's own `emit` that matched the board responder's level `warn` now sees `warning`, with a `subject` when its
|
|
184
|
+
signature names one. A service with `emit=log.append` now logs the board's notices, and those with `notify` (access
|
|
185
|
+
needed, a contested or moved slot, a held request, a reconcile needed) reach the host's banners, without a link to
|
|
186
|
+
the page that resolves them (FAA-187 (21)). A Fabric outage's `board.down` (no notify) is now a row in the feed too,
|
|
187
|
+
said again after each recovery (FAA-187 (22)).
|
|
188
|
+
- A reference service served with `--board-project` registers again with the same `--board-project`
|
|
189
|
+
(`sample_service.py register ... --board-project <uuid>`): a registration from 0.55.0 lacks `board.take-back`, so
|
|
190
|
+
`check_service.py` fails `interop.well-known-capabilities` and the log says `service.manifest.mismatch`. Register
|
|
191
|
+
before you serve: the service checks its manifest only when it starts, so a warning logged before the registration
|
|
192
|
+
stays in the log until the next start.
|
|
193
|
+
- A service that calls `action_gate` (`actionGate`) itself passes a finite `spend_usd` and a string `project`, or
|
|
194
|
+
expects the operator's decision under a mandate with a `perCallMaxUsd` ceiling or a `projects` limit; an `on_fault`
|
|
195
|
+
that matched a job's answer under `fabric.job.get` now matches the job's capability; a descriptor's or provider
|
|
196
|
+
entry's `source.repository` names the repository without a password in its user information (`https:bot:pw@host`
|
|
197
|
+
is refused).
|
|
198
|
+
|
|
3
199
|
## 0.55.0 (2026-10-27)
|
|
4
200
|
|
|
5
201
|
### Added
|
package/README.md
CHANGED
|
@@ -25,7 +25,7 @@ a terminal that works in a project through Fabric. It finds Fabric (`hub.json`,
|
|
|
25
25
|
lifecycle broker start it when it is down, registers `fabric-mcp` once with no secret, calls `fabric_whoami`
|
|
26
26
|
first, reads the project board (`com.status`, `com.list`, `com.read_ack` only what was read) and asks other
|
|
27
27
|
Projects only through `com.submit`, asks who is working on what and claims its task before it starts
|
|
28
|
-
(`fabric_work_list`, `fabric_work_claim`, Fabric
|
|
28
|
+
(`fabric_work_list`, `fabric_work_claim`, Fabric v0.3.5 and later), links tasks to backlog rows instead of copying them, and
|
|
29
29
|
releases at the end (`fabric_work_release`). `scripts/fabric_session.py status` prints where Fabric stands and one
|
|
30
30
|
next action:
|
|
31
31
|
|
|
@@ -38,13 +38,14 @@ resume) puts that preamble into the session when the MCP server `fabric` is regi
|
|
|
38
38
|
uses Fabric (one line pointing at `fabric_whoami` in a folder with no sign of it); when Fabric
|
|
39
39
|
is down it adds one line (skip `fabric_whoami`, start Fabric through the broker if the task needs it), and only in a
|
|
40
40
|
project that uses Fabric; Stop records each turn's facts (HEAD, files changed, lines added and removed, never content)
|
|
41
|
-
and the board's unread count, and, once the running Fabric takes them (
|
|
41
|
+
and the board's unread count, and, once the running Fabric takes them (unless `FABRIC_SESSION_NO_REPOSITORIES=1`), the other repositories the turn touched (it then
|
|
42
42
|
reads the last 512 KB of the session transcript (message text and tool output included, which can hold a secret the session displayed; read in memory only), uses only tool names and path arguments, and sends host/owner/name, never a
|
|
43
43
|
local path or text); UserPromptSubmit adds one line only when that count rose, and each reminder Fabric gave on a turn once (a turn `watch-mail` delivered prints there instead); SessionEnd releases what the
|
|
44
44
|
session left claimed. Stop, UserPromptSubmit and SessionEnd spool and call Fabric only when the running Fabric
|
|
45
45
|
advertises `session-hooks/1`; Stop and SessionEnd always prune their own stale spool folders. All are silent in a session Fabric launched, never block the session, read no Fabric token (an `origin` URL's embedded credential is dropped in memory, never
|
|
46
46
|
kept or sent) and always
|
|
47
|
-
exit 0
|
|
47
|
+
exit 0 once `python3` starts (on Windows, Claude Code runs hooks in Git Bash, or PowerShell without it, where `python3`
|
|
48
|
+
must name Python 3.9 or later; FAA-183). `fabric_session.py watch-mail` waits for board mail under a background watcher, and `install-host-hooks
|
|
48
49
|
--host codex|kimi|hermes|opencode|openclaw|pi` installs each host's native context bridge (dry run unless `--apply`).
|
|
49
50
|
Codex receives structured SessionStart JSON; Hermes, OpenCode, OpenClaw and Pi use native plugin APIs.
|
|
50
51
|
See the [native host matrix](plugins/fabric-agent-adapter/skills/working-with-fabric/references/native-host-hooks.md)
|
package/SECURITY.md
CHANGED
|
@@ -33,9 +33,10 @@ bounded and non-publishing until explicit scoped grants exist in a project bindi
|
|
|
33
33
|
reads by name at run time; the kit itself calls no outside host on its own. A board
|
|
34
34
|
responder talks only to Fabric's hub on loopback.
|
|
35
35
|
- The working-with-fabric session hooks run as the person: Stop and SessionEnd run `git`
|
|
36
|
-
in the session's folder (counts only, at most 1.5 s; with the mark below
|
|
37
|
-
session Fabric launched. Stop also,
|
|
38
|
-
`repositories` (FAA-140; a worker sees it in the tool's schema and leaves a mark)
|
|
36
|
+
in the session's folder (counts only, at most 1.5 s; with the mark below, also that folder's own `origin`, only to leave its repository out), read the process table to tell a
|
|
37
|
+
session Fabric launched. Stop also, only once the running Fabric's `fabric_session_observe` takes
|
|
38
|
+
`repositories` (FAA-140; a worker sees it in the tool's schema and leaves a mark) and unless the person turned it off
|
|
39
|
+
(`FABRIC_SESSION_NO_REPOSITORIES=1` or a `no-repositories` file in the hooks' folder, FAA-160), reads the
|
|
39
40
|
last 512 KB of the session's transcript (earlier turns included; only the entries after the
|
|
40
41
|
last prompt the person typed are used; the tail holds message text and tool output too, file contents and
|
|
41
42
|
command output that can carry a secret the session displayed, read in memory only and never spooled, logged or sent)
|
|
@@ -45,8 +46,8 @@ bounded and non-publishing until explicit scoped grants exist in a project bindi
|
|
|
45
46
|
host, owner and name, never a local path, a command or message text (on a self-hosted ssh
|
|
46
47
|
remote, `owner` is the folder path on that server before the repository's name). The mark is
|
|
47
48
|
removed only by a worker that sees a schema without the field, so after Fabric is downgraded
|
|
48
|
-
the turns that end before a worker next
|
|
49
|
-
names is sent. They write their spool and log under the user's cache folder (0600),
|
|
49
|
+
the turns that end before a worker next reaches the running Fabric (about a minute; longer while
|
|
50
|
+
rejoins fail) are still read; none of their names is sent. They write their spool and log under the user's cache folder (0600),
|
|
50
51
|
and the delivery worker starts `fabric-mcp` in its own process group and ends that group
|
|
51
52
|
(`killpg`, or `taskkill /T /F` on Windows) at its 10 s deadline. They read no Fabric token. An
|
|
52
53
|
`origin` URL that embeds a credential (`https://user:token@host/...`) is read by `git config`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@passioncode-ai/fabric-agent-adapter",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.56.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.
|
|
6
|
+
"version": "0.56.0",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "PassionCode.ai",
|
|
9
9
|
"url": "https://passioncode.ai/"
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"description": "Two silent-unless-relevant hook sets. The dashboard-kit notice: one line at session start or /clear in a repository that owns a Fabric service whose dashboard has no kit or an older one (fabric-dashboard/0.1, DEC-0036), phrased as an upgrade the agent may make itself. The Fabric session hooks (working-with-fabric, COM-04, FAA-12a): at session start or /clear, the preamble when Claude Code has the MCP server `fabric` and Fabric runs (fabric_whoami, then the project board through com.status and com.list, claim and release); when Fabric is down, one line, only in a project that uses Fabric (a Fabric contract file, a project-bound registration or a service descriptor naming the repository); nothing on compact or resume; each turn's facts (HEAD, files changed, lines added and removed, never content), the board's unread count, and, once the running Fabric's fabric_session_observe takes them (
|
|
2
|
+
"description": "Two silent-unless-relevant hook sets. The dashboard-kit notice: one line at session start or /clear in a repository that owns a Fabric service whose dashboard has no kit or an older one (fabric-dashboard/0.1, DEC-0036), phrased as an upgrade the agent may make itself. The Fabric session hooks (working-with-fabric, COM-04, FAA-12a): at session start or /clear, the preamble when Claude Code has the MCP server `fabric` and Fabric runs (fabric_whoami, then the project board through com.status and com.list, claim and release); when Fabric is down, one line, only in a project that uses Fabric (a Fabric contract file, a project-bound registration or a service descriptor naming the repository); nothing on compact or resume; each turn's facts (HEAD, files changed, lines added and removed, never content), the board's unread count, and, once the running Fabric's fabric_session_observe takes them (unless FABRIC_SESSION_NO_REPOSITORIES=1), the other repositories the turn touched (the last 512 KB of the session transcript, message text and tool output included (a secret the session displayed can be there; read in memory only), is then read and only the current turn's tool names and path arguments are used; host/owner/name leave the machine, never a local path or text) and the session's end through Fabric, only when the running Fabric advertises session-hooks/1; at each prompt one line only when the unread count rose, then each nudge a turn's answer carried, once (watch-mail prints those of turns it delivered). All silent in a session Fabric launched. Every hook reads no Fabric token (a credential embedded in an origin URL is read by git config and dropped in memory, never spooled or sent), prints at most one block, always exits 0 once python3 starts (on Windows it must name Python in Git Bash, FAA-183), and hands any Fabric call to a detached worker with a 10 s deadline.",
|
|
3
3
|
"hooks": {
|
|
4
4
|
"SessionStart": [
|
|
5
5
|
{
|
|
@@ -5,7 +5,7 @@ license: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
|
|
|
5
5
|
compatibility: Requires filesystem access and Python 3.9+. Exact schema checks additionally need git, Node.js, pnpm, and the pinned fabric-agent-contract checkout (a public repository). Works without those tools in an explicitly degraded structural-check mode.
|
|
6
6
|
metadata:
|
|
7
7
|
author: PassionCode.ai
|
|
8
|
-
version: "0.
|
|
8
|
+
version: "0.56.0"
|
|
9
9
|
contract-version: "0.1.0"
|
|
10
10
|
contract-commit: "00e050ba7429c0c60261cec871779e0fd6aeeb28"
|
|
11
11
|
---
|
package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_provider.py
CHANGED
|
@@ -47,7 +47,11 @@ ENV_MAX = 64
|
|
|
47
47
|
# Whole-value patterns end in \Z, never `$` (R1: `$` also matches before a trailing newline).
|
|
48
48
|
_ID = re.compile(r"^[a-z][a-z0-9-]{1,62}\Z")
|
|
49
49
|
_ENV_NAME = re.compile(r"^[A-Z_][A-Z0-9_]*\Z")
|
|
50
|
-
|
|
50
|
+
# A special scheme (http, https, ws, wss, ftp) reads a user:password@ after any run of slashes, none included, as
|
|
51
|
+
# `new URL` does: https:bot:pw@host carries the password too (0.56.0 seam r7).
|
|
52
|
+
_URI_PASSWORD = re.compile(r"^(?:(?:[Hh][Tt][Tt][Pp][Ss]?|[Ww][Ss][Ss]?|[Ff][Tt][Pp]):[/\\]*[^/\\?#]*:[^/\\?#]*@"
|
|
53
|
+
# any other scheme: a backslash belongs to the user information (seam r8)
|
|
54
|
+
r"|(?![Hh][Tt][Tt][Pp][Ss]?:|[Ww][Ss][Ss]?:|[Ff][Tt][Pp]:)[A-Za-z][A-Za-z0-9+.-]*://[^/?#]*:[^/?#]*@)")
|
|
51
55
|
_SECRET_REF = re.compile(r"^secret-ref:([A-Za-z0-9][A-Za-z0-9._/:@-]{0,255})\Z")
|
|
52
56
|
_LOCAL_PATH = re.compile(LOCAL_PATH_PATTERN)
|
|
53
57
|
_URL = re.compile(URL_PATTERN)
|
package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_secrets.py
CHANGED
|
@@ -2,12 +2,15 @@
|
|
|
2
2
|
"""The contract's one credential detector (Fabric Agent Contract DEC-0044, `src/secret-shape.ts` at the pin): a name
|
|
3
3
|
that can only carry a credential, a value shaped like one, a launch argument that carries one, and a URL that does
|
|
4
4
|
(LC-21). A port held to the contract's own answers by test/fixtures/contract/profile-vectors.json. Standard library
|
|
5
|
-
only. The dashboard kit's fabric-button.js carries its own copy of the name and value checks (FL-4).
|
|
5
|
+
only. The dashboard kit's fabric-button.js carries its own copy of the name and value checks (FL-4).
|
|
6
|
+
|
|
7
|
+
It reads as the contract's ECMAScript does (FAA-136): ECMAScript's whitespace (Python's `\\s` also takes U+0085 and
|
|
8
|
+
U+001C to U+001F, and not U+FEFF), `.` stopping at every line terminator, ASCII case folding, an ASCII word boundary."""
|
|
6
9
|
|
|
7
10
|
from __future__ import annotations
|
|
8
11
|
|
|
9
12
|
import re
|
|
10
|
-
from typing import Dict, List
|
|
13
|
+
from typing import Dict, List, Tuple
|
|
11
14
|
import urllib.parse
|
|
12
15
|
|
|
13
16
|
CREDENTIAL_WORDS = (
|
|
@@ -22,20 +25,28 @@ CREDENTIAL_PAIRS: Dict[str, tuple] = {
|
|
|
22
25
|
"session": ("token", "id"),
|
|
23
26
|
"code": ("login", "auth", "access", "session"),
|
|
24
27
|
}
|
|
28
|
+
#: ECMAScript's \s, as a character class body.
|
|
29
|
+
JS_SPACE = "\t\n\x0b\x0c\r \xa0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff"
|
|
30
|
+
_NOT_LINE_END = "[^\n\r\u2028\u2029]" # ECMAScript's `.` without the s flag
|
|
25
31
|
JWT_SHAPE = re.compile(r"eyJ[A-Za-z0-9_-]{2,}\.[A-Za-z0-9_-]{2,}\.[A-Za-z0-9_-]*")
|
|
26
|
-
SECRET_VALUE = re.compile(r"(^bearer
|
|
27
|
-
re.IGNORECASE | re.
|
|
32
|
+
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,}"
|
|
33
|
+
r"|AKIA[0-9A-Z]{16}|glpat-[A-Za-z0-9_-]{20,})" % (JS_SPACE, _NOT_LINE_END), re.IGNORECASE | re.ASCII)
|
|
28
34
|
_LONG_RUN = re.compile(r"[A-Za-z0-9+/=_-]{40,}")
|
|
29
|
-
_BEARER_INSIDE = re.compile(r"
|
|
30
|
-
|
|
35
|
+
_BEARER_INSIDE = re.compile(r"(?<![A-Za-z0-9_])bearer[%s]+(?=[A-Za-z0-9._~+/-]*[0-9])[A-Za-z0-9._~+/-]{12,}=*" % JS_SPACE,
|
|
36
|
+
re.IGNORECASE | re.ASCII)
|
|
37
|
+
_PATH_LIKE = re.compile(r"^(?:[/~.]|[A-Za-z]:[\\/]|[a-z][a-z0-9+.-]*://)", re.IGNORECASE | re.ASCII)
|
|
38
|
+
_WORD_SPLIT = re.compile("[%s._-]+" % JS_SPACE)
|
|
39
|
+
_ARGUMENT_SPLIT = re.compile("[%s=]" % JS_SPACE)
|
|
31
40
|
_SCHEME = re.compile(r"^[A-Za-z][A-Za-z0-9+.-]*:")
|
|
41
|
+
#: WHATWG's special schemes that carry an authority with user information (file has none).
|
|
42
|
+
_SPECIAL = ("ftp", "http", "https", "ws", "wss")
|
|
32
43
|
|
|
33
44
|
|
|
34
45
|
def name_words(name: str) -> List[str]:
|
|
35
46
|
"""A name's words: split at `.`, `_`, `-` and camelCase humps, lowercased (`APIKey` is `api key`)."""
|
|
36
47
|
spaced = re.sub(r"([a-z0-9])([A-Z])", r"\1 \2", name)
|
|
37
48
|
spaced = re.sub(r"([A-Z]+)([A-Z][a-z])", r"\1 \2", spaced)
|
|
38
|
-
return [w.lower() for w in
|
|
49
|
+
return [w.lower() for w in _WORD_SPLIT.split(spaced) if w]
|
|
39
50
|
|
|
40
51
|
|
|
41
52
|
def _with_singulars(words: List[str]) -> List[str]:
|
|
@@ -73,7 +84,7 @@ def secret_argument(value: str) -> bool:
|
|
|
73
84
|
so a folder path holding a session id is not taken for a key."""
|
|
74
85
|
if JWT_SHAPE.search(value) or SECRET_VALUE.search(value) or _BEARER_INSIDE.search(value):
|
|
75
86
|
return True
|
|
76
|
-
for piece in
|
|
87
|
+
for piece in _ARGUMENT_SPLIT.split(value):
|
|
77
88
|
if _path_like(piece):
|
|
78
89
|
if any(_long_secret(part) for part in re.split(r"[\\/]", piece)):
|
|
79
90
|
return True
|
|
@@ -93,7 +104,8 @@ def _decode_keeping_plus(raw: str) -> str:
|
|
|
93
104
|
|
|
94
105
|
|
|
95
106
|
def _form_name(text: str) -> str:
|
|
96
|
-
|
|
107
|
+
# URLSearchParams drops one leading `?` from the text it is given (0.56.0 unit r7).
|
|
108
|
+
pairs = urllib.parse.parse_qsl(text[1:] if text.startswith("?") else text, keep_blank_values=True)
|
|
97
109
|
return pairs[0][0] if pairs else ""
|
|
98
110
|
|
|
99
111
|
|
|
@@ -109,23 +121,66 @@ def raw_values(query: str) -> Dict[str, List[str]]:
|
|
|
109
121
|
return values
|
|
110
122
|
|
|
111
123
|
|
|
124
|
+
#: The C0 controls and space, U+0000 to U+0020: what WHATWG URL parsing trims from a URL's ends.
|
|
125
|
+
_C0_OR_SPACE = "".join(map(chr, range(0x21)))
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
_LONE_SURROGATE = re.compile("[\ud800-\udfff]")
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _percent_encode(text: str, also: str) -> str:
|
|
132
|
+
"""WHATWG's percent-encoding of a query or a fragment: every C0 control, space, `"`, `<`, `>`, every code point
|
|
133
|
+
above U+007E and the characters in `also`, as UTF-8 %XX (a lone surrogate as U+FFFD, as a USVString reads it).
|
|
134
|
+
`new URL` hands the contract's secretUrl the text in this form, so a malformed escape left in it is read the same
|
|
135
|
+
way by both ports (0.56.0 unit r8)."""
|
|
136
|
+
out = []
|
|
137
|
+
for char in _LONE_SURROGATE.sub("\ufffd", text):
|
|
138
|
+
if ord(char) <= 0x20 or ord(char) > 0x7E or char in '"<>' or char in also:
|
|
139
|
+
out.append("".join("%%%02X" % byte for byte in char.encode("utf-8")))
|
|
140
|
+
else:
|
|
141
|
+
out.append(char)
|
|
142
|
+
return "".join(out)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _url_parts(value: str) -> Tuple[str, str, str]:
|
|
146
|
+
"""The password, query and fragment of an absolute URL as WHATWG URL parsing splits them, read by hand: urlsplit
|
|
147
|
+
refuses a `[` or `]` outside an IPv6 host (`https://u:p[w@h/`), where `new URL` reads the password (0.56.0 unit
|
|
148
|
+
r7). The fragment starts at the first `#`, the query at the first `?` before it. A special scheme's authority starts
|
|
149
|
+
after every slash or backslash that follows `scheme:` (the special authority ignore slashes state) and ends at the
|
|
150
|
+
next slash, backslash, `?` or `#`, so `https:u:p@h` and `https:\\u:p@h` carry the password p; any other scheme
|
|
151
|
+
has an authority only after `//`, and `file` has no user information. The user information ends at the authority's
|
|
152
|
+
last `@`, and the password follows its first `:`. A host `new URL` would refuse is not checked: such a URL is read
|
|
153
|
+
for its password and parameters all the same, the safe direction for a detector."""
|
|
154
|
+
scheme, _, rest = value.partition(":")
|
|
155
|
+
rest, _, fragment = rest.partition("#")
|
|
156
|
+
rest, _, query = rest.partition("?")
|
|
157
|
+
lowered = scheme.lower()
|
|
158
|
+
if lowered in _SPECIAL:
|
|
159
|
+
authority = rest.replace("\\", "/").lstrip("/").partition("/")[0]
|
|
160
|
+
elif lowered != "file" and rest.startswith("//"):
|
|
161
|
+
authority = rest[2:].partition("/")[0]
|
|
162
|
+
else:
|
|
163
|
+
authority = ""
|
|
164
|
+
userinfo, at, _ = authority.rpartition("@")
|
|
165
|
+
return ((userinfo.partition(":")[2] if at else ""), _percent_encode(query, "'" if lowered in _SPECIAL or lowered == "file" else ""), # file is special too
|
|
166
|
+
_percent_encode(fragment, "`"))
|
|
167
|
+
|
|
168
|
+
|
|
112
169
|
def secret_url(value: str) -> bool:
|
|
113
170
|
"""Whether a URL an MCP entry or a launch names carries a credential (LC-21): anything secret_argument finds, a
|
|
114
171
|
password in its user information, or a query or fragment parameter whose name can only carry a credential and has
|
|
115
172
|
a value, or whose value is shaped like one. A string that is not an absolute URL is checked as an argument only."""
|
|
116
173
|
if secret_argument(value):
|
|
117
174
|
return True
|
|
118
|
-
|
|
175
|
+
# What WHATWG URL parsing does first: trim C0 controls and space from the ends (never other Unicode spaces, which it
|
|
176
|
+
# refuses), then remove every tab, CR and LF.
|
|
177
|
+
value = re.sub(r"[\t\n\r]", "", value.strip(_C0_OR_SPACE))
|
|
178
|
+
if not _SCHEME.match(value):
|
|
119
179
|
return False # not an absolute URL (WHATWG URL parsing refuses it)
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
except ValueError:
|
|
123
|
-
return False
|
|
124
|
-
if not parts.scheme:
|
|
125
|
-
return False
|
|
126
|
-
if parts.password:
|
|
180
|
+
password, query, fragment = _url_parts(value)
|
|
181
|
+
if password:
|
|
127
182
|
return True
|
|
128
|
-
for part in (
|
|
183
|
+
for part in (query, fragment):
|
|
129
184
|
for name, raws in raw_values(part).items():
|
|
130
185
|
if any((raw != "" and credential_name(name)) or secret_value(raw) for raw in raws):
|
|
131
186
|
return True
|
|
@@ -14,7 +14,7 @@ license: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
|
|
|
14
14
|
compatibility: Python 3.9+ or Node.js 20+ for the kits (Node.js 22+ for a Linux service's instance lock); the probe needs Python 3.9+. launchd steps are macOS-only; Linux services install a systemd user unit and Windows ones a per-user Scheduled Task with the kits' writers. Dashboard handoff optionally uses Fabric Dashboards MCP link/host_status/open; without it, report unresolved host capability. The contract checkout is optional.
|
|
15
15
|
metadata:
|
|
16
16
|
author: PassionCode.ai
|
|
17
|
-
version: "0.
|
|
17
|
+
version: "0.56.0"
|
|
18
18
|
contract-version: "0.1.0"
|
|
19
19
|
extension: "fabric-service/0.1"
|
|
20
20
|
extension-commit: "00e050ba7429c0c60261cec871779e0fd6aeeb28"
|
|
@@ -104,8 +104,8 @@ responder in its own process: [`scripts/fabric_hub.py`](scripts/fabric_hub.py) f
|
|
|
104
104
|
every step and stops on `fenced`, `lease_expired` or `authority_changed` (Node: `fabric-hub.mjs`, `fabric-board.mjs`).
|
|
105
105
|
The request body is data behind the service's own input gate. From 0.53.0 a slot the operator moved to another
|
|
106
106
|
session stays moved until `take_back` (`takeBack`): give it an operator action with its tool and gate `relayed`, or it
|
|
107
|
-
stays moved for good (
|
|
108
|
-
or its elicitation). Read [the board reference](references/board.md).
|
|
107
|
+
stays moved for good (the operator reaches it through an agent that relays their decision in `_meta`, the service's
|
|
108
|
+
page or its elicitation; Claude Code and Codex wait for CT-125's forwarding). Read [the board reference](references/board.md).
|
|
109
109
|
|
|
110
110
|
## Step 2: the non-negotiables
|
|
111
111
|
|
|
@@ -260,8 +260,9 @@ or `human` with its `humanStep`) and declared with `build_well_known(actions=ser
|
|
|
260
260
|
`extensions/actions/0.1`. A `relayed` action called by the operator's own agent with their decision in
|
|
261
261
|
`_meta.decision` is the operator's act: never answer it "operator session required" or send them to the page.
|
|
262
262
|
A service adopts the relayed gate once Fabric's hub forwards decisions (CT-125), as the contract says; until then its tools
|
|
263
|
-
answer as today, except the board responder's take-back action, declared `relayed` now
|
|
264
|
-
|
|
263
|
+
answer as today, except the board responder's take-back action, declared `relayed` now: an agent whose client sets
|
|
264
|
+
`_meta` calls it with the operator's decision; Claude Code and Codex reach it through the page until CT-125
|
|
265
|
+
([board.md](references/board.md); the reference service's `board.take-back`). Read the interop reference's *Actions* section for when to adopt, the gate, its hooks and its
|
|
265
266
|
refusals.
|
|
266
267
|
|
|
267
268
|
**Say what the agent uses** (`fabric-connections/0.1`, DEC-0096) so Fabric Dashboards draws its chain: the other
|