@passioncode-ai/fabric-agent-adapter 0.61.0 → 0.63.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 +201 -0
- package/README.md +6 -3
- package/SECURITY.md +3 -1
- 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 +4 -1
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/provider-entry.md +4 -2
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_provider.py +1 -1
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_secrets.py +57 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +11 -9
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/README.md +56 -8
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/channel-agent.example.json +103 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/config.example.json +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/support_agent.py +160 -19
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/connectors.md +4 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/delegation.md +2 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/estate-record.md +2 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/lifecycle.md +4 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/migrating-a-service.md +6 -4
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/package.md +2 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/protocol.md +4 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/remote-placement.md +16 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service.py +18 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service_rules.py +33 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/dashboard_kit.py +2 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-auth.mjs +10 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-delegation.mjs +3 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-descriptor.mjs +33 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-http.mjs +14 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-secrets.mjs +57 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-service.mjs +3 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_auth.py +18 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_descriptor.py +55 -4
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_estate.py +1 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_http.py +5 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_secrets.py +57 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_service.py +4 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/kit.json +193 -13
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/sample-remote-service.mjs +2 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/sample_service.py +42 -4
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +2 -2
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/agents-that-answer-people.md +5 -2
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/practices.md +46 -0
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/SKILL.md +16 -15
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/native-host-hooks.md +7 -1
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/on-demand-session.md +209 -0
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/project-board.md +34 -6
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/session-hooks.md +29 -5
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_cli.py +362 -7
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_env.py +35 -6
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_hosts.py +4 -1
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_mail.py +8 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,206 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.63.0 (2026-11-05)
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- working-with-fabric: a session Fabric started on a board request (FAA-201, Fabric's on-demand spec at `1329b60f`).
|
|
8
|
+
A session is the run only when its own prompt's first line `fabric-dispatch/1 workflow=wf_<16 hex> handoff=<id>
|
|
9
|
+
request=<uuid>` names the same three as `FABRIC_DISPATCH_WORKFLOW`, `FABRIC_DISPATCH_HANDOFF` and
|
|
10
|
+
`FABRIC_DISPATCH_REQUEST` (Switchboard's `dispatch` sets all three and writes the line, SB-247, released in
|
|
11
|
+
Switchboard 0.7.8 on 2026-11-04); the environment alone makes no run, since a reader or a subshell inherits it, and a
|
|
12
|
+
line that disagrees, or an environment without all three, makes none either: that session says so in the request's
|
|
13
|
+
thread and stops (Fabric's spec 3.5 on its main at `deaf51fe`, agreed on the project board, 2026-11-05). A line with no workflow in the
|
|
14
|
+
environment is quoted text. The session-start hook, which sees no prompt yet, prints the exit contract on that
|
|
15
|
+
condition in place of the preamble (naming the session's own `sessionId` when it is a UUID, and after a `/clear`
|
|
16
|
+
the session id the clear replaced, which a replayed accept needs for its `leaseId` and a pending handoff is accepted
|
|
17
|
+
under, or the new id when the replaced one is unknown), a retro before the run ends, also in a session
|
|
18
|
+
Fabric launched: `fabric_whoami`, the packet
|
|
19
|
+
(`observatory_checkpoint_latest`), the request and its thread (`com.get`, `com.list`), the lease
|
|
20
|
+
(`observatory_handoff_accept` with its owner, key and the executor the launcher names), `com.status` and the claim,
|
|
21
|
+
a request an earlier run left accepted or in progress (`com.responder_replace`, `com.reconcile`), in_progress
|
|
22
|
+
checkpoints, renewals about every 20 s (work that outlasts the run gives its state in a `com.progress` note and is left accepted for the next run,
|
|
23
|
+
which Fabric's dispatcher starts since its main at `fff4b6d9`, in no release yet, Fabric itself moving the open
|
|
24
|
+
Observatory workflow to that run, as its spec at `c560d97e` says (sections 3.4 and 3.6; its code does not yet); when the request's deadline is not after
|
|
25
|
+
run-ends-at plus 10 minutes, since no next run starts that close to it (`RUN_END_TO_DEADLINE_MS` on Fabric's main at
|
|
26
|
+
`afe28a60`, in no release yet), it fails known instead), the reply and `com.complete`, and the closing checkpoint with its
|
|
27
|
+
`workflowId` (`done`, or for `failed_known` `failed` once Observatory takes it, OBS-323, until then `blocked` with
|
|
28
|
+
the reason). The prompt hook, which sees the line, tells the session it is the run (or, for a line that disagrees,
|
|
29
|
+
that it is none), and prints no unread line or nudge in such a session. Only the
|
|
30
|
+
first session whose line names the dispatch holds it: another one gets one line
|
|
31
|
+
telling it not to accept, claim, complete or close (the dispatch is told by its `FABRIC_DISPATCH_ID` when the
|
|
32
|
+
launcher sets one in the form `[A-Za-z0-9._-]{1,64}`, as Switchboard 0.7.8 does), and a `/clear` in the holding session keeps the contract (the
|
|
33
|
+
hooks keep one record per run under `dispatch/`, pruned after 7 days by the Claude Code turn and end hooks and by the next session start on a board request). Every
|
|
34
|
+
checkpoint the session writes repeats the goal, constraints, artifacts and credentials of the packet's
|
|
35
|
+
`checkpoint.body`, since Observatory keeps only the latest body; the session takes the request from the environment
|
|
36
|
+
or the first line and its thread from `com.get`, since Observatory's write redacts the packet's board references. A
|
|
37
|
+
Codex run gets the hook only where its `CODEX_HOME` trusts it (Switchboard's isolated homes carry the person's trust since 0.7.8); a host whose hook gets no session id (the native bridges) is always told
|
|
38
|
+
the conditional contract and writes no record; on Windows, where each hook runs under a new Git Bash, a `/clear` holds
|
|
39
|
+
nothing (it cannot tell the holder from a helper), so the cleared session is told it runs inside a run that holds it,
|
|
40
|
+
stays a helper and Fabric takes the workflow over after the run. On any system, a `/clear` while another session
|
|
41
|
+
holds the dispatch (a nested session's clear) is told the same.
|
|
42
|
+
`watch-mail` refuses to start in a dispatched session, and the dashboard-kit notice says nothing there. `references/on-demand-session.md` holds the steps and every argument; the
|
|
43
|
+
hooks tell a dispatched session from this version on.
|
|
44
|
+
- working-with-fabric: whatever waits on the Project is answered in the same turn (FAA-205, the operator's rule of
|
|
45
|
+
2026-11-04, board thread `11646774`): the session-start preamble, the unread line (`N unread on the project board:
|
|
46
|
+
com.list; answer what waits on you in this turn: ...`, its prefix unchanged for watchers) and the skill's board step
|
|
47
|
+
say to answer a request, an announcement or a question with its state now (done, where, when, what blocks) and send
|
|
48
|
+
the result later in a second message; `references/project-board.md` gains "Answering at once".
|
|
49
|
+
- The owner's word is done the first time (FAA-207, the operator's rule of 2026-11-04 relayed on the board, thread
|
|
50
|
+
`a7a9d05d`; the contract's CT-220): working-with-fabric's Step 5 and creating-fabric-agents' practices say the agent
|
|
51
|
+
does it itself, hands the person no command to run, asks one short question on a risk and then acts, and passes on
|
|
52
|
+
only a human gate with its reason; money limits and secrets stay.
|
|
53
|
+
- The channel-agent standard v1 at its round 4 (FAA-197): creating-fabric-agents' practices gain a section of the
|
|
54
|
+
standard's rules for agents on a channel people write to (the ticket as the unit, one outcome per ticket, answer
|
|
55
|
+
rather than route, repair before hand-off, declared sources with a maximum age, hand-off queues with their answered
|
|
56
|
+
signal, text from a channel as data (the contract's proposed CA-8), metadata only, the declaration's form, with a
|
|
57
|
+
holding reply owing a final text, FAC-SEM-164). The support-agent recipe:
|
|
58
|
+
- a hand-off its checks made carries the refused `draft` in the returned decision and to an `on_handoff` the agent
|
|
59
|
+
passes, never into a file (`decisions.jsonl` or the default outbox), so a repeat of the id returns it without; `answer(new_id, text, repair=decision)` rewrites it once with the refusal's reasons: a pass is
|
|
60
|
+
a reply with `repaired: true`, and a repair that ends otherwise (a second refusal, a model `HANDOFF`, no knowledge on
|
|
61
|
+
the retry; not a `model:*` outage) a hand-off with `fallback: true` (the standard's 4.4 and its ticket's
|
|
62
|
+
`fallback`), which `repair=` then refuses, as it refuses a repair under the refused decision's own message id;
|
|
63
|
+
- `KnowledgeSource` (or `knowledgeMaxAgeSeconds` in the config, which reads the file at once) reads the knowledge
|
|
64
|
+
file as a source: read again when it changes (its time, size, inode or status-change time, the last a creation time on Windows), the last good version kept when a read fails or the file has the
|
|
65
|
+
wrong shape (nested too deep included), `state()` with the age of that version, from its file's modification
|
|
66
|
+
time;
|
|
67
|
+
- `Knowledge.load` refuses a file that is not JSON, one nested deeper than 32 levels (on every Python), a file that is not an object with an `entries` list, an entry whose tags are not a list
|
|
68
|
+
of text or whose question is not text, each as a `RecipeError`;
|
|
69
|
+
- `channel-agent.example.json`, one project's declaration under
|
|
70
|
+
`https://fabric.passioncode.ai/agent-contract/extensions/channel-agent/0.1` in the round 4 form, held to it and to
|
|
71
|
+
the example config's maximum age by `test/test_channel_agent_example.py`.
|
|
72
|
+
|
|
73
|
+
### Upgrading
|
|
74
|
+
|
|
75
|
+
- A recopied `support_agent.py` refuses at start (exit 1, a `RecipeError` naming the entry or the file's shape) a knowledge file 0.62.0
|
|
76
|
+
took in other ways, so an agent that ran on 0.62.0 can stop at start now. Per shape, 0.62.0: a file that is not an
|
|
77
|
+
object with an `entries` list (`[]`, `{}`, a string) loaded as an empty set and handed every message off as
|
|
78
|
+
`no-knowledge`; `{"entries": null}` or `{"entries": <number>}` stopped it at start with a `TypeError`; an entry with `tags: null`
|
|
79
|
+
failed each message with a `TypeError`; an entry whose `tags` is a string or holds something other than text, or
|
|
80
|
+
whose `question` is not text, was read and answered from. A file nested deeper than 32 levels, which 0.62.0
|
|
81
|
+
loaded, is refused too. Fix the file before upgrading.
|
|
82
|
+
- A custom `on_handoff` that stores the row it is given now receives the refused `draft`, which can echo what the
|
|
83
|
+
customer wrote: drop `draft` before storing it (the default outbox does).
|
|
84
|
+
- A session carrying `FABRIC_DISPATCH_WORKFLOW` now gets the exit contract at session start, on the condition that its
|
|
85
|
+
own prompt starts with the dispatch line naming the same three, even when Fabric launched it; with the workflow
|
|
86
|
+
set but the handoff or the request missing or malformed, it gets one line that no session there is a run. Its prompt hook prints no unread line or reminder (the nudges file
|
|
87
|
+
stays), and `watch-mail` refuses to start there. A launcher that sets the workflow alone starts no run any more.
|
|
88
|
+
- The live docs name a closed module neutrally, as the organization edition (`references/package.md` in
|
|
89
|
+
building-fabric-services and two backlog rows), at its owner's request; dated reports and handoffs keep their words.
|
|
90
|
+
- A session that does a board request with no person in its chat stops before a production step and names it for a
|
|
91
|
+
person, unless the operator's own words, relayed verbatim with their date and source from one of his own projects (provenance `trusted`) and with a scope naming the step, approve it (working-with-fabric `references/project-board.md`; board threads `1add3c03` and `24e0c88e`; Fabric's board rule at `108239c0`).
|
|
92
|
+
|
|
93
|
+
### Fixed
|
|
94
|
+
|
|
95
|
+
- Every annotation in the shipped scripts (the kit, the skills' scripts, the recipe and the Hermes adapter) resolves
|
|
96
|
+
with `typing.get_type_hints` (FAA-203, a board finding, thread `9fd84d47`): `fabric_estate.py` and four working-with-fabric modules import the `typing` names they use, and
|
|
97
|
+
`LoginCodes`' `token` is written `Optional[Union[str, Callable[[], str]]]`, which Python 3.9 reads. A vendored copy
|
|
98
|
+
of `fabric_estate.py` or `fabric_auth.py` reads as older; it worked at run time before.
|
|
99
|
+
|
|
100
|
+
## 0.62.0 (2026-11-04)
|
|
101
|
+
|
|
102
|
+
A new install names the repository of its code, and a service says what it uses, so a map of the installed agents can
|
|
103
|
+
link each to its project and draw its chain (FAA-198, FAA-199).
|
|
104
|
+
|
|
105
|
+
### Added
|
|
106
|
+
|
|
107
|
+
- The reference service declares its connections (`extensions/connections/0.1`): the template it drafts from and the
|
|
108
|
+
demo source `serve --degraded` marks failing; the online example (`sample-remote-service.mjs`) declares an empty list,
|
|
109
|
+
since it calls nothing outside itself. The support-agent recipe's `SupportAgent.connections()` names its two models
|
|
110
|
+
for the service that runs it, keeping only an id the connections rule accepts, and imports `fabric_connections` only
|
|
111
|
+
when called (the recipe's install line names the module). `check_service.py` reports `well-known.connections` as `NOT_RUN` when the
|
|
112
|
+
well-known document has no connections declaration (it reported nothing before); an empty list is one and reads
|
|
113
|
+
`PASS`, and adapting-projects-to-fabric names the declaration in its
|
|
114
|
+
adapter step (FAA-199).
|
|
115
|
+
|
|
116
|
+
### Changed
|
|
117
|
+
|
|
118
|
+
- `write_descriptor` / `writeDescriptor` refuse a new descriptor that names no `source.repository` (no descriptor of its
|
|
119
|
+
`id.instance` is installed yet: an installed one is a regular file, whatever it holds, so a folder, a broken link or
|
|
120
|
+
a path that cannot be examined is none), with `ServiceError` naming the descriptor; `register_remote` / `registerRemote` refuse it before they
|
|
121
|
+
write the token file, so a refused registration leaves nothing, and a rotation that names no `source` keeps the
|
|
122
|
+
installed one when the kit accepts it (a hand-edited one it refuses is dropped, as before). Rewriting a descriptor already installed without it still writes it and warns
|
|
123
|
+
(`NoSourceRepositoryWarning`, a `UserWarning` attributed to the installer's call; Node `process.emitWarning`, code
|
|
124
|
+
`FABRIC_NO_SOURCE_REPOSITORY`), so an installed agent's update keeps working. `source_refusal` / `sourceRefusal` say
|
|
125
|
+
why an install would be refused, for an installer that checks before it validates or writes anything else (a `source`
|
|
126
|
+
that is no object names nothing), and `names_repository` / `namesRepository` whether a descriptor names one.
|
|
127
|
+
`check_service.py` names a descriptor without it, or with one the descriptor's rules refuse, and why
|
|
128
|
+
(`descriptor.source-repository`, `NOT_RUN`, never a pass), and prints a repository without its userinfo, query or
|
|
129
|
+
fragment (a URL through `redact_url`; a local path, one that starts with `/`, `~`, `.`, `\\` or a drive, by its name;
|
|
130
|
+
a value of a URL scheme such as `https:` also without `//`, through `redact_url`; an scp form or any other value from
|
|
131
|
+
after its user and password, whatever they hold; one with a piece shaped like a credential that is no name not at
|
|
132
|
+
all), as
|
|
133
|
+
`state.outside-code` and `dashboard.opens-in-host` now do too for the source and for the data checkout's origin (they
|
|
134
|
+
printed them as found).
|
|
135
|
+
`sample_service.py register` writes this repository, whose code its descriptor runs whatever `--id` names, takes
|
|
136
|
+
`--repository`, and writes its manifest only after the descriptor, so a refusal of any kind leaves none naming it, and
|
|
137
|
+
a manifest it cannot write takes a new descriptor back with it, or puts an installed one's bytes back; an installed
|
|
138
|
+
descriptor it cannot read is never replaced (it refuses before writing anything). No owner field is added: the
|
|
139
|
+
organization follows from the repository (FAA-198). `source.repository` is read for a credential by one rule in both kits and
|
|
140
|
+
in the provider entry's check (`repository_credential` / `repositoryCredential` in `fabric_secrets`): its user, its query
|
|
141
|
+
and fragment as `secret_url` reads them (a parameter named for a credential, such as `access_token`, now counts), and
|
|
142
|
+
a path segment shaped like a key; a path segment in lower case, or of letters with no digit, is the repository's
|
|
143
|
+
name unless a key's shape (such as `sk-`, `ghp_`, `xoxb-`, `glpat-`, `AKIA`, `eyJ`) starts it or follows a `.`,
|
|
144
|
+
`_` or `-` in it, so `task-pipeline-dashboard` and `Task-Pipeline-Dashboard`, which read as an `sk-` key, are accepted (a source with it was refused before). A user is accepted only in a URL's
|
|
145
|
+
authority (`https://user@host/path`): an scp form or an opaque URI holding an `@` (`bot:...@host:path`,
|
|
146
|
+
`https:user@host/path`) is refused, and so is a password in the user information read any way a reader takes it:
|
|
147
|
+
as written (`https://h\u:pw@x/r`, as git reads it) or as the URL standard does (`https:///u:pw@h/r`). Node reads
|
|
148
|
+
the user information and parameters by hand, as Python does, so a special scheme written without `//`
|
|
149
|
+
(`https:host/r?access_token=...`) or a host `new URL` refuses is read alike.
|
|
150
|
+
|
|
151
|
+
### Fixed
|
|
152
|
+
|
|
153
|
+
- `redact_url` / `redactUrl` (the URL an `HttpError` carries and `check_url` / `checkUrl` print) dropped a user
|
|
154
|
+
information only as the URL standard reads it, so `https://h\u:pw@x/r`, which git and Python's `urllib` read as a
|
|
155
|
+
password, printed `pw` in its path in both kits (0.61.0 too); they now drop the user information as
|
|
156
|
+
written first, after any run of slashes or backslashes that follows the scheme (`https:/h\u:pw@x/r`,
|
|
157
|
+
`https:\\h\u:pw@x/r`; 0.62.0 seam readings 11 and 13).
|
|
158
|
+
- Node's `checkUrl` sent `ht<tab>tps://h\u:pw@x/r` and `https:\/h\u:pw@x/r`, which Python's `check_url` refuses
|
|
159
|
+
(since 0.60.0): it now reads the written user information on the text `new URL` reads, after any run of slashes.
|
|
160
|
+
- Node's `redactUrl` printed `<unparseable url>` for a URL of a scheme other than http, https, ws, wss or ftp (`file:`
|
|
161
|
+
included) that `new URL` cannot parse (`ssh://[zz]/r`, `ssh://h:99999/r`, `file://h:1/r`), where Python keeps the
|
|
162
|
+
scheme: both now print `<scheme>:<redacted>`.
|
|
163
|
+
- A subscription `runner.kind` that is no string (a list or an object) raised `TypeError` in Python's
|
|
164
|
+
`validate_descriptor`, `write_descriptor` and `check_service.py` (0.61.0 too), and Node, for a list whose text is a
|
|
165
|
+
subscription runner's name (`["claude-code"]`, which `Object.hasOwn` reads as that key), named only one of its two
|
|
166
|
+
problems: both kits now name both as descriptor problems.
|
|
167
|
+
- Node's key set cache (`JwksCache`) accepted `https://h\u:pw@x/j` and `https://@x/j`, which the Python twin refuses:
|
|
168
|
+
it now reads the authority as written too.
|
|
169
|
+
|
|
170
|
+
### Upgrading
|
|
171
|
+
|
|
172
|
+
- An installer that writes a descriptor for a new install (`register_remote` / `registerRemote` included) and a desktop
|
|
173
|
+
app that writes its own (DEC-0069) pass `source: {"repository": "https://github.com/owner/name"}` (the repository
|
|
174
|
+
their code is built from) in the call that writes it: a source set by a later write is too late, since the first call
|
|
175
|
+
is refused before anything is written. An installer that loads the kit from the installed plugin meets this as
|
|
176
|
+
soon as the plugin updates; an installed agent's next update warns until it names one. Only a rewrite in
|
|
177
|
+
place keeps working: an update that removes the descriptor before it writes it again, a new instance (a preview) and a
|
|
178
|
+
moved services directory are new installs. In Python the warning is `NoSourceRepositoryWarning` (a `UserWarning`), so
|
|
179
|
+
a suite that runs with warnings as errors (`-W error`) sees the rewrite raise; filter that class there.
|
|
180
|
+
- Once a descriptor names its repository, two probe rules that read `NOT_RUN` without it start judging:
|
|
181
|
+
`state.outside-code` fails data kept inside that checkout, and `dashboard.opens-in-host` reads the checkout for pages
|
|
182
|
+
opened in a browser. A service that passed may fail them until its data moves out of the checkout or its pages open
|
|
183
|
+
through Fabric Dashboards.
|
|
184
|
+
- Declare what the service uses with `connections_extension` / `connectionsExtension` (an empty list when it uses
|
|
185
|
+
nothing): until it does, `well-known.connections` reads `NOT_RUN` and the connection map has no row for it.
|
|
186
|
+
- A `source.repository` (a service's or a provider entry's) that names a user outside a URL's authority (an scp form,
|
|
187
|
+
`https:user@host/path`) or carries a query parameter named for a credential is refused: write it as
|
|
188
|
+
`https://github.com/owner/name`, with no user, query or fragment. A path segment that mixes upper case with a digit
|
|
189
|
+
is read for a key's shape (`sk-` and 16 more characters, a `ghp_` run, 40 characters of mixed case and digits), so
|
|
190
|
+
`Task-Pipeline-Dashboard2` is refused while `Agent-V2` is not: write the repository's name in lower case (hosts such
|
|
191
|
+
as GitHub take either). A segment that a key's shape starts, or follows a `.`, `_` or `-` in, is refused whatever
|
|
192
|
+
its case and also percent-encoded (`%73k-` reads as `sk-`). The shapes are those of `fabric_secrets.SECRET_VALUE`,
|
|
193
|
+
such as `sk-` and 16 more characters, `gh` and one of p, o, u, s, r then `_` and 20, `xox` and one of a, b, o, p, r,
|
|
194
|
+
s then `-` and 10 (so the word `xoxo-` counts), `glpat-` and 20, `AKIA` and 16, and a JWT as written (`eyJ` in
|
|
195
|
+
that case, `JWT_SHAPE`); `bearer` and a space counts at a segment's start once decoded (`Bearer%20Tracking` is
|
|
196
|
+
refused, `team-Bearer%20Tracking` stays a name), and anywhere in a segment or a host when 12 or more token
|
|
197
|
+
characters with a digit follow it and no letter, digit or `_` comes before it (`x-Bearer%20<a token>` is refused).
|
|
198
|
+
A host is read for a key's shape anywhere in it, inside a word too, so `task-pipeline-dashboard.example.com` is
|
|
199
|
+
refused although the same path segment is a name (FAA-200 (32)). Such a host, like a repository such
|
|
200
|
+
as `sk-learn-` followed by a longer name, has no spelling the kit accepts, so an agent built from it is not installed
|
|
201
|
+
new, and
|
|
202
|
+
an installed one keeps working by a rewrite in place that leaves the source out (FAA-200 (24)).
|
|
203
|
+
|
|
3
204
|
## 0.61.0 (2026-11-04)
|
|
4
205
|
|
|
5
206
|
The board responder reads the operator's mandate from a request of his own Project (FAA-135), and working-with-fabric
|
package/README.md
CHANGED
|
@@ -23,7 +23,8 @@ The skills are `adapting-projects-to-fabric`, `creating-fabric-agents`,
|
|
|
23
23
|
`working-with-fabric` is for the other side: a coding agent (Claude Code, Codex, Kimi Code, Kilo, Hermes) opened in
|
|
24
24
|
a terminal that works in a project through Fabric. It finds Fabric (`hub.json`, its `pid`), has the machine's
|
|
25
25
|
lifecycle broker start it when it is down, registers `fabric-mcp` once with no secret, calls `fabric_whoami`
|
|
26
|
-
first, reads the project board (`com.status`, `com.list`, `com.read_ack` only what was read)
|
|
26
|
+
first, reads the project board (`com.status`, `com.list`, `com.read_ack` only what was read), answers what waits on its
|
|
27
|
+
Project in the same turn with its state (the result later; a request a responder should take stays queued), and asks other
|
|
27
28
|
Projects only through `com.submit`, asks who is working on what and claims its task before it starts
|
|
28
29
|
(`fabric_work_list`, `fabric_work_claim`, Fabric v0.3.5 and later), links tasks to backlog rows instead of copying them, and
|
|
29
30
|
releases at the end (`fabric_work_release`). `scripts/fabric_session.py status` prints where Fabric stands and one
|
|
@@ -37,12 +38,14 @@ In Claude Code the plugin adds four session hooks: SessionStart (at startup and
|
|
|
37
38
|
resume) puts that preamble into the session when the MCP server `fabric` is registered, Fabric runs and the project
|
|
38
39
|
uses Fabric (one line pointing at `fabric_whoami` in a folder with no sign of it); when Fabric
|
|
39
40
|
is down it adds one line (skip `fabric_whoami`, start Fabric through the broker if the task needs it), and only in a
|
|
40
|
-
project that uses Fabric;
|
|
41
|
+
project that uses Fabric; in a session Fabric started on a board request (`FABRIC_DISPATCH_WORKFLOW` set) it prints the
|
|
42
|
+
exit contract instead, on the condition that the session's own prompt starts with the dispatch line naming the same
|
|
43
|
+
three, and UserPromptSubmit, which sees that line, confirms the run or says it is none (FAA-201); Stop records each turn's facts (HEAD, files changed, lines added and removed, never content)
|
|
41
44
|
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
45
|
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
46
|
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
47
|
session left claimed. Stop, UserPromptSubmit and SessionEnd spool and call Fabric only when the running Fabric
|
|
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
|
|
48
|
+
advertises `session-hooks/1`; Stop and SessionEnd always prune their own stale spool folders. All are silent in a session Fabric launched (except the exit contract in one started on a board request, where the prompt hook prints no unread line or reminder), never block the session, read no Fabric token (an `origin` URL's embedded credential is dropped in memory, never
|
|
46
49
|
kept or sent) and always
|
|
47
50
|
exit 0 once `python3` starts (on Windows, Claude Code runs hooks in Git Bash, or PowerShell without it, where `python3`
|
|
48
51
|
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
|
package/SECURITY.md
CHANGED
|
@@ -47,7 +47,9 @@ bounded and non-publishing until explicit scoped grants exist in a project bindi
|
|
|
47
47
|
remote, `owner` is the folder path on that server before the repository's name). The mark is
|
|
48
48
|
removed only by a worker that sees a schema without the field, so after Fabric is downgraded
|
|
49
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
|
+
rejoins fail) are still read; none of their names is sent. They write their spool and log under the user's cache folder (0600), and, in a session
|
|
51
|
+
started on a board request (FAA-201), one record of which session holds that dispatch (the holding session's id and, after a `/clear`, the id it replaced, the agent's process id and a time, 0600,
|
|
52
|
+
pruned after 7 days),
|
|
51
53
|
and the delivery worker starts `fabric-mcp` in its own process group and ends that group
|
|
52
54
|
(`killpg`, or `taskkill /T /F` on Windows) at its 10 s deadline. They read no Fabric token. An
|
|
53
55
|
`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.63.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.63.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 (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.",
|
|
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. In a session Fabric started on a board request (FABRIC_DISPATCH_WORKFLOW), session start prints the exit contract instead of the preamble on the condition that the session's own prompt starts with the dispatch line naming the same three, launched or not, and the prompt hook, which sees that line, confirms the run or says it is none, launched or not, with no unread line or nudge, and the dashboard-kit notice says nothing (FAA-201).",
|
|
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.63.0"
|
|
9
9
|
contract-version: "0.1.0"
|
|
10
10
|
contract-commit: "3830a3353923e20f826d466610e1c479f120bec0"
|
|
11
11
|
---
|
|
@@ -143,6 +143,9 @@ Preserve the chosen ownership boundary:
|
|
|
143
143
|
`building-fabric-services`'s `scripts/fabric_interop.py`.
|
|
144
144
|
- Local runner maps typed input, executable identity, result location, cancellation,
|
|
145
145
|
heartbeat, and partial results without relying on ambient accounts.
|
|
146
|
+
- A service declares what it uses (`extensions/connections/0.1`: models, other agents, connectors, stores,
|
|
147
|
+
channels) with `connections_extension` / `connectionsExtension`, and its descriptor names `source.repository`;
|
|
148
|
+
the probe names either one missing (FAA-198, FAA-199).
|
|
146
149
|
|
|
147
150
|
Keep model selection and provider-internal reasoning outside the Fabric contract. Expose
|
|
148
151
|
typed outcomes, evidence, artifacts, and protocol-visible state, not chain-of-thought.
|
package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/provider-entry.md
CHANGED
|
@@ -57,8 +57,10 @@ It refuses, with one sentence that never quotes a value:
|
|
|
57
57
|
(`test_provider.SchemaParityTests` compares them with a contract checkout at the pin when `FABRIC_CONTRACT_NEW` names
|
|
58
58
|
one, as the contract job of the nightly CI does), and the reader is held to every `provider.schema.json` fixture of
|
|
59
59
|
the pin (`test/fixtures/contract/provider-fixtures.json`, `test_provider.ContractFixtureTests`);
|
|
60
|
-
- a `source.repository` with a `user:password
|
|
61
|
-
|
|
60
|
+
- a `source.repository` with a `user:password@`, a user outside a URL's authority, or a value shaped like a credential
|
|
61
|
+
in its user, query or fragment or as a path segment (one in lower case, or of letters with no digit, is the name unless a key's shape starts it or follows a `.`, `_` or `-`, read as written and percent-decoded; a host is read for a key's shape anywhere in it,
|
|
62
|
+
inside a word too, so `task-pipeline-dashboard.example.com` is refused, FAA-200 (32)): it names the
|
|
63
|
+
repository, never reaches it (since 0.49.3; one rule with the service kit's descriptor check since 0.62.0);
|
|
62
64
|
- a manifest whose name follows a backslash (`C:\Agents\example\fabric-agent.json`). The reader,
|
|
63
65
|
`validate_provider_entry`, accepts it since DEC-0145 (`MANIFEST_PATTERN`, the schema's), but a reader that has not
|
|
64
66
|
re-pinned to DEC-0145 refuses the entry, Fabric's registry among them (CT-192), so the writer does not emit it until
|
package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_provider.py
CHANGED
|
@@ -147,7 +147,7 @@ def validate_provider_entry(entry: Any) -> List[str]:
|
|
|
147
147
|
problems.append("source is {repository?}")
|
|
148
148
|
elif "repository" in source and not _is_uri(source["repository"]):
|
|
149
149
|
problems.append("source.repository is an absolute URI")
|
|
150
|
-
elif "repository" in source and (_URI_PASSWORD.match(source["repository"]) or _secrets.
|
|
150
|
+
elif "repository" in source and (_URI_PASSWORD.match(source["repository"]) or _secrets.repository_credential(source["repository"])):
|
|
151
151
|
# FAA-120, as the service kit: a repository is named, never reached with a credential in its URI.
|
|
152
152
|
problems.append("source.repository carries no password or credential: it names the repository")
|
|
153
153
|
if not isinstance(entry["installedAt"], str) or not _TIMESTAMP.fullmatch(entry["installedAt"]):
|
package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_secrets.py
CHANGED
|
@@ -185,3 +185,60 @@ def secret_url(value: str) -> bool:
|
|
|
185
185
|
if any((raw != "" and credential_name(name)) or secret_value(raw) for raw in raws):
|
|
186
186
|
return True
|
|
187
187
|
return False
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
_REPOSITORY_URI = re.compile(r"\A([A-Za-z][A-Za-z0-9+.-]*:(?://[^/?#]*)?)([^?#]*)(.*)\Z", re.S)
|
|
191
|
+
_REPOSITORY_NAME = re.compile(r"(?:[a-z0-9._-]+|[A-Za-z._-]+)\Z") # lower case, or letters with no digit (readings 8)
|
|
192
|
+
_REPOSITORY_AUTHORITY = re.compile(r"[A-Za-z][A-Za-z0-9+.-]*://")
|
|
193
|
+
_KEY_AT_A_WORD = re.compile(r"(?:\A|(?<=[._-]))(?:%s)" % SECRET_VALUE.pattern, re.IGNORECASE | re.ASCII)
|
|
194
|
+
_JWT_AT_A_WORD = re.compile(r"(?:\A|(?<=[._-]))%s" % JWT_SHAPE.pattern)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def repository_name(piece: str) -> bool:
|
|
198
|
+
"""Whether a path segment reads as a repository's name: lower case, or letters with no digit, with no provider
|
|
199
|
+
key's or JWT's shape (`sk-`, `ghp_`, `xoxb-`, `AKIA`, `eyJ`) at its start or after a `.`, `_` or `-` (0.62.0
|
|
200
|
+
readings 9 and 10), so a letters-only key is still a key while `task-pipeline-dashboard`, which holds an `sk-` shape
|
|
201
|
+
inside a word, is a name."""
|
|
202
|
+
return bool(_REPOSITORY_NAME.match(piece)) and not _KEY_AT_A_WORD.search(piece) and not _JWT_AT_A_WORD.search(piece)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def ascii_decoded(text: str) -> str:
|
|
206
|
+
"""Every `%XX` escape of an ASCII character decoded, the rest left as written: how a repository's path and user
|
|
207
|
+
information are read as well as written, since git and hosts decode them (`%73k-` is `sk-`, `u%3Apw` is `u:pw`;
|
|
208
|
+
0.62.0 readings 11). The Node twin is asciiDecoded."""
|
|
209
|
+
return re.sub(r"%([0-7][0-9A-Fa-f])", lambda escape: chr(int(escape.group(1), 16)), text)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def repository_password(value: str) -> bool:
|
|
213
|
+
"""Whether a repository URI's user information holds a password, in any scheme, `file` too: every slash and
|
|
214
|
+
backslash after the colon skipped, then up to the next `/`, `?` or `#`, a backslash kept in the user. That one
|
|
215
|
+
reading holds git's (`https://h\\u:pw@x/r`), curl's (`https:///h\\u:pw@x`) and the URL standard's
|
|
216
|
+
(`https:///u:pw@h`, which stops at a backslash, a prefix of it) (0.62.0 readings 10 to 14). The user information
|
|
217
|
+
ends at the last `@`; a password follows its first `:`, as written or ascii_decoded. The Node twin is
|
|
218
|
+
repositoryPassword."""
|
|
219
|
+
value = re.sub(r"[\t\n\r]", "", value.strip(_C0_OR_SPACE))
|
|
220
|
+
scheme, colon, rest = value.partition(":")
|
|
221
|
+
authorities = [re.match(r"[^/?#]*", rest.lstrip("/\\")).group(0)] if colon else []
|
|
222
|
+
users = [authority.rpartition("@")[0] for authority in authorities]
|
|
223
|
+
return any(user.partition(":")[2] for user in users + [ascii_decoded(user) for user in users])
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def repository_credential(value: str) -> bool:
|
|
227
|
+
"""FAA-198 (0.62.0 readings 6 and 7): whether a repository URI carries a credential: in its scheme and authority (a
|
|
228
|
+
user), in its query or fragment as secret_url reads them, or as a path segment shaped like a key. A path segment in
|
|
229
|
+
lower case, or of letters with no digit (task-pipeline-dashboard, Task-Pipeline-Dashboard), is the repository's
|
|
230
|
+
name, never a key, though it reads as an `sk-` shape, unless a key's shape starts it or follows a `.`, `_` or `-` in
|
|
231
|
+
it (repository_name); a segment is read as written and ascii_decoded. A
|
|
232
|
+
user outside a URL's authority (an scp form, `https:user@host/path`) counts as one: a user is an authority's."""
|
|
233
|
+
if "@" in value and not _REPOSITORY_AUTHORITY.match(value):
|
|
234
|
+
return True
|
|
235
|
+
match = _REPOSITORY_URI.match(value)
|
|
236
|
+
if not match:
|
|
237
|
+
return secret_argument(value)
|
|
238
|
+
head, path, tail = match.groups()
|
|
239
|
+
if repository_password(value):
|
|
240
|
+
return True
|
|
241
|
+
odd = [piece for written in (path, ascii_decoded(path)) for piece in re.split(r"[/\\:]", written)
|
|
242
|
+
if piece and not repository_name(piece)]
|
|
243
|
+
# the scheme and authority decoded too: git sends `https://%67hp_...@h` with its user decoded (unit reading 12)
|
|
244
|
+
return secret_url(head + tail) or secret_argument(ascii_decoded(head)) or any(secret_argument(piece) for piece in odd)
|
|
@@ -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.63.0"
|
|
18
18
|
contract-version: "0.1.0"
|
|
19
19
|
extension: "fabric-service/0.1"
|
|
20
20
|
extension-commit: "3830a3353923e20f826d466610e1c479f120bec0"
|
|
@@ -65,16 +65,17 @@ Answer before writing code, and record the answers in the project README:
|
|
|
65
65
|
| What it stores | the store, where it lives (Step 2, rule 5), and what survives uninstall |
|
|
66
66
|
| Data and uptime | kind U or O, kit folders, a durable store or `<id>-data`, uptime A or B; a new agent's `LICENSE` holder, transfer audit per release (creating-fabric-agents) |
|
|
67
67
|
| What it tells the operator | the events worth a notification (Step 5) |
|
|
68
|
+
| Source | `source.repository`, required (Step 4); connections, `[]` if none |
|
|
68
69
|
|
|
69
70
|
## Step 1: choose the surfaces
|
|
70
71
|
|
|
71
72
|
| Caller | Surface | Why |
|
|
72
73
|
|---|---|---|
|
|
73
|
-
| Another agent on this machine | MCP `2026-07-28`, `streamable-http`, at `/mcp` on the service origin, token in a header | one process serves every session
|
|
74
|
+
| Another agent on this machine | MCP `2026-07-28`, `streamable-http`, at `/mcp` on the service origin, token in a header | one process serves every session (stdio-per-session once left twenty stray servers) |
|
|
74
75
|
| The operator or a script | a CLI: `<tool> service status\|start\|stop\|restart`, `<tool> dashboard` (opens only in Fabric Dashboards, below), `<tool> doctor --json` | every verb delegates to launchd and the well-known document, never to its own process table |
|
|
75
76
|
| A remote agent | A2A `1.0` over HTTPS, or MCP behind an authenticated gateway | loopback reachability is not authorization |
|
|
76
77
|
| An A2A peer on this computer | `surfaces.a2a` names a public Agent Card, served without the token; its interface URLs are loopback (DEC-0122); the kits read it, serving one is FAA-10 | the card carries no credential, process member or required Fabric extension |
|
|
77
|
-
| The dashboard page | same-origin REST under `/api`, session cookie plus a custom request header; every button on it also a tool on the MCP surface (Step 5) | the
|
|
78
|
+
| The dashboard page | same-origin REST under `/api`, session cookie plus a custom request header; every button on it also a tool on the MCP surface (Step 5) | the existing dashboards' CSRF defence |
|
|
78
79
|
|
|
79
80
|
Read [the surfaces and auth reference](references/surfaces-and-auth.md) when wiring
|
|
80
81
|
MCP registration into a client config, the login flow or the CSRF header.
|
|
@@ -213,8 +214,8 @@ descriptor's `fabricManifest` at a manifest whose service key names this
|
|
|
213
214
|
The installer, not the service, owns the plist and the descriptor. Sequence:
|
|
214
215
|
|
|
215
216
|
1. `write_descriptor(descriptor)`: refuses a port or `id.instance` another descriptor
|
|
216
|
-
claims
|
|
217
|
-
one more service the operator sees
|
|
217
|
+
claims, and a new one without `source.repository` (a rewrite warns). A preview or branch
|
|
218
|
+
copy is a second **instance**, never a second id: one more service the operator sees. Verify a release with its
|
|
218
219
|
own doctor or self-check before it replaces `default`; a temporary instance is uninstalled
|
|
219
220
|
(`launchd_uninstall`, `remove_descriptor`) as soon as its check is done, not left running.
|
|
220
221
|
2. `launchd_plist(...)` then `launchd_install(...)`: writes and lints the plist,
|
|
@@ -269,7 +270,7 @@ refusals.
|
|
|
269
270
|
**Say what the agent uses** (`fabric-connections/0.1`, DEC-0096) so Fabric Dashboards draws its chain: the other
|
|
270
271
|
agents, models, connectors, stores and channels, each with its state, from
|
|
271
272
|
`connections_extension()` / `connectionsExtension()` in `fabric_connections`. Never a credential in it, never a call
|
|
272
|
-
count (that is the usage report);
|
|
273
|
+
count (that is the usage report); details: [the wire reference](references/protocol.md).
|
|
273
274
|
|
|
274
275
|
The events feed is a **view over the log you already keep** (a jobs table, a JSONL
|
|
275
276
|
journal), not a second store. Each event is one sentence a person reads; set
|
|
@@ -298,7 +299,8 @@ rule it can reach and says which it could not:
|
|
|
298
299
|
python3 <skill-dir>/scripts/check_service.py <id>[.<instance>]
|
|
299
300
|
```
|
|
300
301
|
|
|
301
|
-
It reports `PASS`, `FAIL` or `NOT_RUN` per rule (descriptor, port claim,
|
|
302
|
+
It reports `PASS`, `FAIL` or `NOT_RUN` per rule (descriptor, port claim, `descriptor.source-repository`,
|
|
303
|
+
`well-known.connections`, well-known
|
|
302
304
|
shape, identity, latency, Host/Origin/cross-site guards, loopback bind, token file,
|
|
303
305
|
events auth and shape, single-use login, state outside code, instance lock, plist,
|
|
304
306
|
launchd pid equals answering pid, `dashboard.declared` (ED-9), `well-known.actions` (DEC-0121),
|
|
@@ -405,8 +407,8 @@ this estate needs.
|
|
|
405
407
|
- **Not macOS / no launchd:** the kits write a systemd user unit (Linux) or a per-user Scheduled Task (Windows); with
|
|
406
408
|
no supervisor at all declare `lifecycle.manager: "none"`, and hosts show the service read-only. Keep every other rule.
|
|
407
409
|
- **A desktop app with no supervisor** (DEC-0069): its descriptor says `manager: "person"` with `lifecycle.app`, no
|
|
408
|
-
origin, `installedBy {by: "person", role, tool}
|
|
409
|
-
|
|
410
|
+
origin, `installedBy {by: "person", role, tool}`, `source.repository` (the lifecycle reference, *An app's own
|
|
411
|
+
descriptor and the node*).
|
|
410
412
|
- **No Python:** use the Node kit; run the probe from any machine that has Python and
|
|
411
413
|
can reach the port, or mark the probe `NOT_RUN` with the reason.
|
|
412
414
|
- **Neither kit fits the framework:** implement the four routes and the lock by hand
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/README.md
CHANGED
|
@@ -22,9 +22,14 @@ service that calls it owns its channel (mail, a chat, a store review) and its re
|
|
|
22
22
|
This recipe is the draft-and-check step of an agent that answers people, not that agent, and used alone it falls short
|
|
23
23
|
of one. Every hand-off goes to `on_handoff`, by default the outbox a person reads, at the first refusal: the practices
|
|
24
24
|
name that as the failing pattern (P2). An agent that repairs passes its own `on_handoff=` so a failed check reaches
|
|
25
|
-
its repair step rather than a person; the
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
its repair step rather than a person; a hand-off the checks made carries the failed checks in `reasons` and the
|
|
26
|
+
refused `draft`, and the repair step passes that decision back as `answer(new_id, text, repair=decision)`: the model
|
|
27
|
+
gets its draft and the reasons, and a pass comes back as a reply with `repaired: true`, a second refusal as a hand-off
|
|
28
|
+
with `fallback: true` (the channel-agent standard's 4.4 and its ticket's `fallback`, FAA-197). It calls under a new message id, since the stored decision answers any repeat of the same id (`repair=` under the
|
|
29
|
+
refused decision's own id is refused with a `RecipeError`); `repair=` takes only a hand-off the checks
|
|
30
|
+
made (not `no-knowledge`, `model-unsure` or `model:*`), and never a repeat: a repeat of the id returns the stored
|
|
31
|
+
decision, which has no draft, so the repair step keeps the decision it was handed and passes each first decision to
|
|
32
|
+
`repair=` once. A repair the model declines (`HANDOFF`) or that finds no knowledge is marked `fallback: true` too: it marks a spent repair; a `model:*` outage is not one. `on_handoff` runs under the agent's lock, before the decision is saved, so it queues the
|
|
28
33
|
hand-off and returns: since 0.55.0 a call to `answer` from inside it, on the same thread, is refused with
|
|
29
34
|
`ReentryError` (a `RecipeError`), and unless `on_handoff` catches it the first call raises it too, its draft paid and
|
|
30
35
|
nothing saved or delivered. A call it hands to another thread and waits for waits for the lock forever, so it never
|
|
@@ -42,8 +47,8 @@ are not counted. The recipe hands off nothing for `policy`: the calling service
|
|
|
42
47
|
handle (refunds, unbans, account recovery) before it calls the recipe, or a model's `HANDOFF` on them is counted as a
|
|
43
48
|
false knowledge gap. Count its hand-offs by cause: a failed check is a refusal for the agent to repair, research and
|
|
44
49
|
hold, and only a hold on the material counts as `inability` (a hold after an outage or at the research time cap is
|
|
45
|
-
`infrastructure`); `unsupported:amount` is a money reason, never researched, and so is `unsupported:number`
|
|
46
|
-
|
|
50
|
+
`infrastructure`); `unsupported:amount` is a money reason, never researched, and so is `unsupported:number` unless the
|
|
51
|
+
refused `draft` shows the number is not a price; `no-knowledge` and `model-unsure` are `inability`;
|
|
47
52
|
every `inability` opens a gap; `model:*` is `infrastructure`, and `model:ceiling` and `model:payment` are also the
|
|
48
53
|
turn's `limit` outcome. The rest of the agent's parts with checks come from [agents that answer
|
|
49
54
|
people](../../../creating-fabric-agents/references/agents-that-answer-people.md): the turn record, the daily
|
|
@@ -71,13 +76,48 @@ names the same file.
|
|
|
71
76
|
|
|
72
77
|
## Install into a service
|
|
73
78
|
|
|
79
|
+
The service that runs it says what it uses (FAA-199): `agent.connections()` names the two models, and the service adds
|
|
80
|
+
its own hand-off route, in its well-known document:
|
|
81
|
+
`fs.connections_extension(agent.connections() + [...], degraded=degraded)`. The two take the ids `model` and `fallback`
|
|
82
|
+
(a `degraded` entry whose source is `fallback` marks the fallback); the service's own items take other ids, since one id
|
|
83
|
+
named twice is refused.
|
|
84
|
+
|
|
74
85
|
```bash
|
|
75
86
|
python3 <adapter>/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_kit.py \
|
|
76
|
-
install <service>/support --lang py --module fabric_openrouter --module fabric_usage --module fabric_paths
|
|
87
|
+
install <service>/support --lang py --module fabric_openrouter --module fabric_usage --module fabric_paths \
|
|
88
|
+
--module fabric_connections
|
|
77
89
|
cp <adapter>/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/support_agent.py <service>/support/
|
|
78
90
|
python3 <adapter>/.../fabric_kit.py status <service>/support # current, older or modified, per file
|
|
79
91
|
```
|
|
80
92
|
|
|
93
|
+
## The knowledge file as a source
|
|
94
|
+
|
|
95
|
+
The channel-agent standard declares every source with its owner, how it is read, its cadence and its maximum age, and a source that fails
|
|
96
|
+
keeps its last good version and says so (its 7.1 and 7.4). `KnowledgeSource(path, max_age_s=...)` is the knowledge
|
|
97
|
+
file read that way: it reads the file again when it changes, keeps the last good version when a read fails, and
|
|
98
|
+
`state()` gives `{id, age_s, max_age_s, stale, degraded}`. The age is the last good version's, from its file's
|
|
99
|
+
modification time: a file nobody rebuilt grows old however often it is read, and a restart does not make it new.
|
|
100
|
+
The standard ages a source from its last finished sync, so the knowledge build writes or touches the file at every
|
|
101
|
+
successful rebuild, unchanged or not, and never keeps the source's time (`cp -p`, `rsync -t`). Pass it as `knowledge=`, or set
|
|
102
|
+
`knowledgeMaxAgeSeconds` in the config, which reads the file at once (a file never read well fails there). The calling service shows `state()`
|
|
103
|
+
on its page and opens one alert when `stale` turns true: the recipe has no page and sends no alert itself.
|
|
104
|
+
|
|
105
|
+
## Declaring the channel-agent standard
|
|
106
|
+
|
|
107
|
+
An agent that answers people on a channel declares what it follows under
|
|
108
|
+
`https://fabric.passioncode.ai/agent-contract/extensions/channel-agent/0.1` (the contract's proposed DEC-0150, not
|
|
109
|
+
merged yet, so the key and the form may change), in its `fabric-agent.json` or its
|
|
110
|
+
well-known `extensions` (the standard's 11 and 13.6; the doctrine: creating-fabric-agents
|
|
111
|
+
[practices](../../../creating-fabric-agents/references/practices.md#agents-on-a-channel-people-write-to)).
|
|
112
|
+
`channel-agent.example.json` is one project's declaration in the standard's round 4 form: a required member that is
|
|
113
|
+
not there yet reads `{"none": "not_yet", "until": "<date>"}`, one not used `{"none": "not_used"}`, never null except
|
|
114
|
+
where nothing is decided (a channel's `decision`); a final text never `not_used` beside a holding reply with replies
|
|
115
|
+
on (FAC-SEM-164); `recommendation` only with `answered_signal: "none"`, the id of the
|
|
116
|
+
board thread where the queue's owner was told; a channel's `items_seen_unit` one of `mails`, `updates`, `messages`,
|
|
117
|
+
`reviews`; durations in ISO 8601; the knowledge file as a source with its
|
|
118
|
+
maximum age. `test/test_channel_agent_example.py` holds the example to that form; `check_service.py` reads the
|
|
119
|
+
declaration once the contract publishes its schema.
|
|
120
|
+
|
|
81
121
|
## Configure
|
|
82
122
|
|
|
83
123
|
`config.example.json`, paths relative to the config's folder:
|
|
@@ -85,6 +125,7 @@ python3 <adapter>/.../fabric_kit.py status <service>/support # current, old
|
|
|
85
125
|
| Key | Meaning |
|
|
86
126
|
|---|---|
|
|
87
127
|
| `knowledge` | the knowledge file: `{"entries": [{"id", "question", "answer", "tags"}]}`, ids unique |
|
|
128
|
+
| `knowledgeMaxAgeSeconds` | optional: the knowledge file is then a source with an age and a last good version (above) |
|
|
88
129
|
| `stateDir` | the decisions (`decisions.jsonl`), the handoff outbox (`handoffs.jsonl`) and the usage ledger (`usage.jsonl`) |
|
|
89
130
|
| `model`, `fallback` | two models of different vendors; a lane that reads what customers write runs on an open-vendor model (DEC-0083) |
|
|
90
131
|
| `keyEnv` | the environment variable that holds the agent's own OpenRouter key; never a value in the file |
|
|
@@ -104,7 +145,11 @@ the store's runner, which puts the named key into this one process's environment
|
|
|
104
145
|
`python3 "$(project-observatory full-path)/tools/use_secret.py" run --vault-only <project> OPENROUTER_API_KEY --
|
|
105
146
|
python3 support_agent.py answer ...`); a long-running
|
|
106
147
|
service gets it the same way from its service manager. The decision
|
|
107
|
-
prints as JSON: `{"messageId", "action": "reply"|"handoff", "reply"?, "citations", "reasons", "costUsd", "repeated"}
|
|
148
|
+
prints as JSON: `{"messageId", "action": "reply"|"handoff", "reply"?, "citations", "reasons", "costUsd", "repeated"}`,
|
|
149
|
+
with `"draft"` on a hand-off the checks made, `"repaired": true` on a reply after `repair=` and `"fallback": true` on
|
|
150
|
+
a repair that ended in a second refusal, a declined repair (`HANDOFF`) or no knowledge; a `model:*` outage during a
|
|
151
|
+
repair is a plain hand-off without a draft. Two meanings of the word: the config's `fallback` is the second model; a decision's `fallback: true`
|
|
152
|
+
is the channel-agent standard's fallback, a spent repair (a second refusal, a declined repair or no knowledge on the retry, never a `model:*` outage). `repair=` refuses a decision that is already `fallback: true`.
|
|
108
153
|
Exit 1 on a broken config or knowledge file. A missing key is not a crash: the message is handed off with
|
|
109
154
|
`model:config`.
|
|
110
155
|
|
|
@@ -119,7 +164,10 @@ Every handoff row in `handoffs.jsonl` holds the customer's own message, which ca
|
|
|
119
164
|
number or a card number. It stays until it is pruned. Choose a retention the product's privacy policy states, at most
|
|
120
165
|
30 days for an agent that answers people (the standard's A1). A1 also asks for the text redacted: the outbox keeps it
|
|
121
166
|
as written, and `prune` takes any `--keep-days`, so such an agent passes an `on_handoff` that redacts what it stores,
|
|
122
|
-
which replaces the outbox, and keeps 30 days or fewer.
|
|
167
|
+
which replaces the outbox, and keeps 30 days or fewer. A refused `draft`, which can echo what the customer wrote,
|
|
168
|
+
reaches the returned decision and an `on_handoff` the agent passes, never a file: neither `decisions.jsonl` nor the
|
|
169
|
+
default outbox stores it. A capability handler returns the decision without it: a kit job's result is written to
|
|
170
|
+
the job's file (`fabric_interop`'s job store), so pass `draft` only to the repair step. Prune on a schedule:
|
|
123
171
|
|
|
124
172
|
```bash
|
|
125
173
|
python3 support_agent.py prune --config config.json --keep-days 30
|