@passioncode-ai/fabric-agent-adapter 0.59.0 → 0.60.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 +53 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/plugins/fabric-agent-adapter/.claude-plugin/plugin.json +1 -1
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/SKILL.md +1 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +4 -4
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/board.md +35 -13
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/kit-modules.md +1 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/package.md +7 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-session.mjs +9 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-slots.mjs +80 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-package.mjs +6 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_session.py +11 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_slots.py +80 -4
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_package.py +14 -6
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/kit.json +97 -7
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/sample_service.py +2 -2
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +1 -1
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/SKILL.md +5 -5
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/project-board.md +21 -7
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_env.py +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,58 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.60.0 (2026-11-03)
|
|
4
|
+
|
|
5
|
+
The board responder is ready to find a slot the operator moves back to it, once Fabric offers the move (FAA-165), and four smaller fixes: working-with-fabric
|
|
6
|
+
reads a filed row's answer (FAA-169) and says when an accepted request is left behind by a move (FAA-172), `pack`
|
|
7
|
+
names why a provider manifest is refused (FAA-193), and Python's package kit refuses `NaN` and `Infinity` as Node's does.
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- A slot the operator moved to another session and then back to this one (contract DEC-0140 PR-2, once Fabric offers
|
|
12
|
+
the move) would be held but never claimed or renewed: the responder skipped a `moved` slot until `take_back`, so its lease lapsed. Now, while a slot
|
|
13
|
+
is moved, the responder reads `com.status` once a renewal (20 s) and, when the slot's generation is past the recorded
|
|
14
|
+
move, sends one `com.claim` under a fresh key, as PR-2's "Learning the slot is its own" says. A claim that lands
|
|
15
|
+
holds the slot again (the record loses `moved`, the request it handed out is worked, or its own enroll renews the
|
|
16
|
+
lease at once; a later move is said again); `lease_expired` enrolls again; `held` asks at the next renewal;
|
|
17
|
+
`idempotency_window_expired` or `capacity_exceeded` asks again at the next renewal under a fresh key, recording
|
|
18
|
+
nothing; `fenced` or another refusal that is not the board session's records the new generation, so the same move is not asked about
|
|
19
|
+
twice. A recorded move drops the capability's claim key, so a claim answer lost before the move is never replayed, and a
|
|
20
|
+
lost answer to the move-back claim itself is asked again under its key, counting as a lease from its send meanwhile (while more than the 10 s margin of that lease is left)
|
|
21
|
+
(a replay from an older generation is dropped); `not_authorized` counts as another session's. It never enrolls or replaces a
|
|
22
|
+
slot another session holds, and waits while other work runs. Both kits (`fabric_board_slots.py`,
|
|
23
|
+
`fabric_board_session.py` and their Node twins). The `board.slot.moved` notice and references/board.md say so (FAA-165).
|
|
24
|
+
- `pack`, `sign` and `verify` refused a provider manifest whose `costPresets` differ from the package's with only
|
|
25
|
+
`provider-manifest` as the message; the message now names both causes (the `problem` code is unchanged) (not a JSON object, or `costPresets` that differ). Both kits (FAA-193, found by the
|
|
26
|
+
FAA-124 pilot rehearsal). Python no longer refuses an empty provider manifest (`{}`), as Node never did (FAA-194 (1)).
|
|
27
|
+
- Python's `pack`, `sign` and `verify` took `NaN`, `Infinity` and `-Infinity` in a plugin or provider manifest, a key file
|
|
28
|
+
or a trust file, which `JSON.parse` and the contract refuse, so a package the Node kit refused could pass Python's
|
|
29
|
+
`verify`. Python refuses them now too (older than 0.60.0, since 0.31.0; found by the 0.60.0 seam reading 11).
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
|
|
33
|
+
- working-with-fabric Step 6 reads `fabric_task_create`'s answer as Step 4 item 2 does: while your single live claim is held, Fabric
|
|
34
|
+
0.3.14 links what is filed as spawned from it, so an unrelated row is filed after the release, or the person is told
|
|
35
|
+
it is linked (FAA-169). A scenario eval `a-backlog-row-filed-while-a-claim-is-held`.
|
|
36
|
+
- working-with-fabric references/project-board.md, "When nobody answers": an `active` responder does not prove a request
|
|
37
|
+
is worked; one the old holder accepted before its slot moved stays `accepted` with nobody on it, and a requester
|
|
38
|
+
cannot see that (`com.get` shows no attempt), so it asks on the thread; with no answer it cancels the request while
|
|
39
|
+
its `effect` is `not_started` and goes on only once the cancel's receipt shows `cancelled`. An accepted request is
|
|
40
|
+
never adapted by hand and never expires (FAA-172; question to Fabric and the contract on board thread `ab12642e`). A
|
|
41
|
+
scenario eval `an-accepted-request-left-behind-by-a-move`.
|
|
42
|
+
- building-fabric-services SKILL.md: a moved slot stays moved until `take_back` or a move back once Fabric offers one.
|
|
43
|
+
- The reference service's take-back tool now reads "Take back a board slot moved to another session."; a manifest
|
|
44
|
+
registered before 0.60.0 keeps the old description until it is registered again.
|
|
45
|
+
|
|
46
|
+
### Upgrading
|
|
47
|
+
|
|
48
|
+
- No Fabric release offers the operator's move of a slot yet (Fabric 0.3.15 has none): today a moved slot is
|
|
49
|
+
another session's taking it, and take-back is the way back; the move-back check is ready for when Fabric ships it.
|
|
50
|
+
- A kit service gets the move back once it installs the 0.60.0 kit modules (`fabric_board_slots.py`,
|
|
51
|
+
`fabric-board-slots.mjs` and their callers). Only the running session finds a move back: a slot moved back to a
|
|
52
|
+
session that has since ended needs take-back, since the session's end released the slot and the operator cannot move
|
|
53
|
+
a slot no session holds (a move is possible only once another session holds it). A kit older than 0.60.0 never finds
|
|
54
|
+
a move back: it needs take-back (any kit from 0.53.0), or, once Fabric offers the move, the 0.60.0 modules and then a move to the new session.
|
|
55
|
+
|
|
3
56
|
## 0.59.0 (2026-11-03)
|
|
4
57
|
|
|
5
58
|
Portable from day 0 (FAA-191, RM-36): on top of 0.58.0's source and instance, every new agent gets a data home, a
|
package/README.md
CHANGED
|
@@ -72,7 +72,7 @@ requests other Projects post to its Project on Fabric's project board uses the b
|
|
|
72
72
|
(`scripts/fabric_hub.py` and `scripts/fabric_board.py`, Node `fabric-hub.mjs` and `fabric-board.mjs`): the
|
|
73
73
|
`board.respond` grant, enroll, claim, accept the exact digest, complete, a checkpoint before every step, and restarts
|
|
74
74
|
that reconcile or take accepted work back by a transfer, unless the slot was moved to another session (until the
|
|
75
|
-
operator's take-back) (the [board reference](plugins/fabric-agent-adapter/skills/building-fabric-services/references/board.md)):
|
|
75
|
+
operator's take-back, or a move back once Fabric offers one) (the [board reference](plugins/fabric-agent-adapter/skills/building-fabric-services/references/board.md)):
|
|
76
76
|
|
|
77
77
|
```bash
|
|
78
78
|
python3 plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service.py example-agent
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@passioncode-ai/fabric-agent-adapter",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.60.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.60.0",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "PassionCode.ai",
|
|
9
9
|
"url": "https://passioncode.ai/"
|
|
@@ -5,7 +5,7 @@ license: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
|
|
|
5
5
|
compatibility: Requires filesystem access and Python 3.9+. Exact schema checks additionally need git, Node.js, pnpm, and the pinned fabric-agent-contract checkout (a public repository). Works without those tools in an explicitly degraded structural-check mode.
|
|
6
6
|
metadata:
|
|
7
7
|
author: PassionCode.ai
|
|
8
|
-
version: "0.
|
|
8
|
+
version: "0.60.0"
|
|
9
9
|
contract-version: "0.1.0"
|
|
10
10
|
contract-commit: "3830a3353923e20f826d466610e1c479f120bec0"
|
|
11
11
|
---
|
|
@@ -14,7 +14,7 @@ license: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
|
|
|
14
14
|
compatibility: Python 3.9+ or Node.js 20+ for the kits (Node.js 22+ for a Linux service's instance lock); the probe needs Python 3.9+. launchd steps are macOS-only; Linux services install a systemd user unit and Windows ones a per-user Scheduled Task with the kits' writers. Dashboard handoff optionally uses Fabric Dashboards MCP link/host_status/open; without it, report unresolved host capability. The contract checkout is optional.
|
|
15
15
|
metadata:
|
|
16
16
|
author: PassionCode.ai
|
|
17
|
-
version: "0.
|
|
17
|
+
version: "0.60.0"
|
|
18
18
|
contract-version: "0.1.0"
|
|
19
19
|
extension: "fabric-service/0.1"
|
|
20
20
|
extension-commit: "3830a3353923e20f826d466610e1c479f120bec0"
|
|
@@ -103,9 +103,9 @@ When the service answers requests other Projects post to its Project on Fabric's
|
|
|
103
103
|
responder in its own process: [`scripts/fabric_hub.py`](scripts/fabric_hub.py) finds the hub and joins,
|
|
104
104
|
[`scripts/fabric_board.py`](scripts/fabric_board.py) enrolls, claims, accepts the exact digest, checkpoints before
|
|
105
105
|
every step and stops on `fenced`, `lease_expired` or `authority_changed` (Node: `fabric-hub.mjs`, `fabric-board.mjs`).
|
|
106
|
-
The request body is data behind the service's own input gate.
|
|
107
|
-
session stays moved until `take_back` (`takeBack`): give it an operator action with its tool and gate
|
|
108
|
-
|
|
106
|
+
The request body is data behind the service's own input gate. A slot moved to another
|
|
107
|
+
session stays moved until `take_back` (`takeBack`) or a move back once Fabric offers one: give it an operator action with its tool and gate
|
|
108
|
+
`relayed` (the operator reaches it through an agent that relays their decision in `_meta`, the service's
|
|
109
109
|
page or its elicitation; Claude Code and Codex wait for CT-125's forwarding). Read [the board reference](references/board.md).
|
|
110
110
|
|
|
111
111
|
## Step 2: the non-negotiables
|
|
@@ -120,7 +120,7 @@ once the kit reads it (FAA-135).
|
|
|
120
120
|
|---|---|
|
|
121
121
|
| `fenced` | the attempt is another responder's: stop the handler, drop the entry (nothing outside had begun); accepted work on a slot this responder recorded holding stays as `lapsed`, and the next tick asks `com.status` whether the slot [moved](#when-the-operator-moves-a-slot-to-another-session-faa-155) |
|
|
122
122
|
| `lease_expired` on accepted work | stop the handler; the board holds the request, and the kit takes it back with `com.responder_replace` and a transfer |
|
|
123
|
-
| `authority_changed`, `not_authorized` | stop claiming; one `board.access.needed` event (notify) saying what to ask for |
|
|
123
|
+
| `authority_changed`, `not_authorized` | stop claiming; one `board.access.needed` event (notify) saying what to ask for (except `not_authorized` on the move-back claim of a moved slot, read as another session's slot, [below](#when-the-operator-moves-a-slot-to-another-session-faa-155)) |
|
|
124
124
|
| `fenced` on enroll or claim | another live holder: wait for it to lapse; never replace it on its own initiative. When this responder recorded holding the slot, it first reads `com.status`: a newer generation means the slot [moved](#when-the-operator-moves-a-slot-to-another-session-faa-155), and it is not taken again even after that holder lapses |
|
|
125
125
|
| lost after an effect began | keep the entry as `unknown`; `observe(entry)` is asked to settle it with `com.reconcile` |
|
|
126
126
|
| Fabric down, hub missing | bounded backoff (2 s doubling to 5 minutes, jittered), one `board.down` event, no spinning |
|
|
@@ -134,7 +134,7 @@ the target Project's participant reply answered it, DEC-0098 and DEC-0102; not s
|
|
|
134
134
|
it, but no longer this responder's); `in_progress` and `outcome_unknown` are
|
|
135
135
|
reconciled from what `observe(entry)` answers (observed only; with no observation they stay unknown and notify);
|
|
136
136
|
accepted ones go back to this responder with `com.responder_replace {expectedGeneration, transfer}` (unless the slot
|
|
137
|
-
moved to another session meanwhile: they stay `lapsed` until take-back, [below](#when-the-operator-moves-a-slot-to-another-session-faa-155)), whose receipt's
|
|
137
|
+
moved to another session meanwhile: they stay `lapsed` until take-back or a move back to this session, [below](#when-the-operator-moves-a-slot-to-another-session-faa-155)), whose receipt's
|
|
138
138
|
`transferred` (DEC-0078: `{request, revision, attempt}`) carries the new fence. **A Fabric that does not answer
|
|
139
139
|
`transferred` yet leaves the request held** (`board.request.held`, notify): the kit never claims it back, because the
|
|
140
140
|
contract has no `com.claim` read-back and `com.claim` never moves an accepted request. A known outcome is sent after the
|
|
@@ -181,7 +181,7 @@ contract's matrix, held to its vectors (`test/fixtures/contract/comms-hold-vecto
|
|
|
181
181
|
| Refused `held` | What the kit does |
|
|
182
182
|
|---|---|
|
|
183
183
|
| `com.status` names the slot `held: true` at connect | the slot is not enrolled; it pauses |
|
|
184
|
-
| `com.enroll`, `com.claim`, `com.responder_replace` | the slot pauses: asked again after 15 s, then 30 s, doubling to 15 minutes; one `board.slot.held` event per capability, and `board.slot.released` when an enroll succeeds again |
|
|
184
|
+
| `com.enroll`, `com.claim`, `com.responder_replace` | the slot pauses: asked again after 15 s, then 30 s, doubling to 15 minutes; one `board.slot.held` event per capability, and `board.slot.released` when an enroll succeeds again (except the move-back claim of a moved slot, which asks again at the next renewal with no `board.slot.held`, [below](#when-the-operator-moves-a-slot-to-another-session-faa-155)) |
|
|
185
185
|
| `com.accept` (the hold came after the claim) | nothing was done: the entry is dropped, the claim lapses and the request waits on the board (`board.request.waiting`); the slot pauses |
|
|
186
186
|
| `com.effect_begin` | the effect has not begun: the handler waits (same idempotency key, the pause doubling) while the tick keeps renewing the lease, then begins once the hold is lifted; `ctx.stopped` ends the wait |
|
|
187
187
|
| accepted work at restart under a hold | kept as `lapsed`, then taken back with `com.responder_replace` and a transfer once the hold is lifted, never claimed back |
|
|
@@ -194,7 +194,7 @@ whose responder was gone. Read it as the owner's attestation, not the responder'
|
|
|
194
194
|
|
|
195
195
|
## When the operator moves a slot to another session (FAA-155)
|
|
196
196
|
|
|
197
|
-
The contract's DEC-0140 PR-2 (
|
|
197
|
+
The contract's DEC-0140 PR-2 (accepted 2026-10-30, after this kit's pin) lets the operator move a
|
|
198
198
|
(Project, capability) slot to another live session of the Project: one transition, as `com.responder_replace` would make
|
|
199
199
|
it for the receiving session (generation up, `claimed` back to `queued`, `in_progress` to `outcome_unknown`, `accepted`
|
|
200
200
|
held and not transferred). The responder keeps that move instead of undoing it:
|
|
@@ -228,17 +228,18 @@ held and not transferred). The responder keeps that move instead of undoing it:
|
|
|
228
228
|
this service left unanswered, is a move, whatever made it: the operator, or another session that took the slot while
|
|
229
229
|
this one was away or after it crashed. Before 0.53.0 a restarted responder took such a slot back by itself. So a
|
|
230
230
|
second responder of the same capability is no longer a standby once the slot changed hands: after any change of
|
|
231
|
-
holder it waits for take-back, not for the other's lapse.
|
|
231
|
+
holder it waits for take-back or a move back to its session, not for the other's lapse.
|
|
232
232
|
- A `fenced` refusal on accepted work while the slot did not move (the generation unchanged) keeps the entry
|
|
233
233
|
`lapsed`; the next tick takes it back with a transfer and goes on (before 0.53.0 the entry was dropped and the
|
|
234
234
|
request left accepted on the board).
|
|
235
235
|
- Then the slot is `moved`: the running handler is told to stop (`ctx.stopped`); a `claimed` entry is dropped (the move
|
|
236
236
|
returned it to the queue); accepted work stays as `lapsed` and work whose effect began as `unknown` (only the slot's
|
|
237
|
-
holder, or the estate owner, reconciles it now).
|
|
238
|
-
`com.
|
|
237
|
+
holder, or the estate owner, reconciles it now). While the slot is moved nothing but the move-back check
|
|
238
|
+
(`com.status` once a renewal, then one `com.claim`, below) is sent for that capability: no `com.renew`,
|
|
239
|
+
`com.complete`, `com.enroll` or `com.responder_replace`, not at recovery after a restart, and not when the new
|
|
239
240
|
holder's lease lapses, nor when com.status later names no generation for the slot (a recorded move stays until
|
|
240
|
-
take-back). Accepted requests stay `accepted` on the board, unanswered, until the slot is taken back
|
|
241
|
-
will hand them to the new holder). One `board.slot.moved` event (warning, notify) says so, once, also across restarts
|
|
241
|
+
take-back or a move back). Accepted requests stay `accepted` on the board, unanswered, until the slot is taken back
|
|
242
|
+
or moved back (CT-175 will hand them to the new holder). One `board.slot.moved` event (warning, notify) says so, once, also across restarts
|
|
242
243
|
(the record keeps `moved`).
|
|
243
244
|
- A responder that never held the slot (a first start: no record, or a record of another Project), or whose record
|
|
244
245
|
equals the current generation, behaves as before: it enrolls, recovers and replaces its own lapsed work. A recorded or
|
|
@@ -248,7 +249,29 @@ held and not transferred). The responder keeps that move instead of undoing it:
|
|
|
248
249
|
records its own generation. Only a replay the board refuses, or one that ran for the first time (`repeated: false`)
|
|
249
250
|
after the slot had moved past the record, reads as a move: the safe side; a slot such a replay took is left to lapse
|
|
250
251
|
unused. A generation written `2.0` is `2`.
|
|
251
|
-
-
|
|
252
|
+
- The operator may move the slot back to this session in Fabric (DEC-0140 PR-2), which this client cannot hear. No
|
|
253
|
+
Fabric release offers the operator's move yet (Fabric 0.3.15 has none: the control is CT-174's in Fabric, its agent twin CT-181 in the contract, then CT-125),
|
|
254
|
+
so today every move is another session taking the slot and take-back is the route back; the check below is ready
|
|
255
|
+
for the move. While
|
|
256
|
+
a slot is moved the responder reads `com.status` once a renewal (20 s) and, when the slot's generation is past the
|
|
257
|
+
recorded move, sends one `com.claim` under a fresh key, as PR-2's "Learning the slot is its own" says (FAA-165,
|
|
258
|
+
0.60.0); the move itself drops the claim key of that capability, whose receipt the move made dead. A claim that
|
|
259
|
+
lands is the slot held again: the record loses `moved`, the request it handed out is worked (with none, its own
|
|
260
|
+
enroll renews the lease at once), accepted work left `lapsed` comes back with a transfer, and a later move is said
|
|
261
|
+
again. `lease_expired` (the move reached this session and its lease lapsed first) enrolls again. `held` asks again
|
|
262
|
+
at the next renewal. `idempotency_window_expired` or `capacity_exceeded` asks again at the next renewal under a
|
|
263
|
+
fresh key, recording nothing. `fenced`, or any other refusal that is not the board session's (`not_authorized` counted with them, since a
|
|
264
|
+
non-holder's claim may be refused so), is another session's slot:
|
|
265
|
+
recorded as `moved` at that generation, so it is not asked about twice. A claim whose answer was lost (`unreachable`) is asked again under the same key at the next renewal, so a claim that
|
|
266
|
+
landed is not stranded; until then it counts as at most a lease from that key's first send (a fresh key starts
|
|
267
|
+
its own, and it counts only while more than the 10 s margin of that lease is left, then no more, as a lapsed slot), so a second lost answer keeps
|
|
268
|
+
the session (finding 38). A replayed receipt from a generation older than `com.status` shows (a move away and back meanwhile) is
|
|
269
|
+
dead: its key is dropped and a fresh key asks at the next renewal. A take-back drops the capability's claim key too, so a request a move-back claim handed out while the take-back raced
|
|
270
|
+
it waits for its attempt's lease (60 s) before it is claimed again (a take-back that lands before the move-back check makes its claim key
|
|
271
|
+
leaves that key to the claim loop, which works the request at once). Nothing is asked while the responder stops. It is never an enroll or a replace on a slot another session holds, and it waits
|
|
272
|
+
while other work runs: a move back during a job longer than the lease (60 s) may lapse and be lost to another
|
|
273
|
+
session, after which take-back, or another move back to this session, brings it back.
|
|
274
|
+
- Otherwise only the operator brings the slot back: `take_back(capability)` (`takeBack`) forgets the record, and wins over a move
|
|
252
275
|
being read at the same time: the move is written under the responder's lock (Node: on the fence queue, after any fenced
|
|
253
276
|
call in flight) and only while the record is unchanged (FAA-158). Never edit the
|
|
254
277
|
checkpoint's `slots` record by hand to undo a move. In the same board
|
|
@@ -269,9 +292,8 @@ held and not transferred). The responder keeps that move instead of undoing it:
|
|
|
269
292
|
tool](../SKILL.md), `fabric-actions/0.1`) with gate `relayed` (the operator's decision), never `none`, so a call
|
|
270
293
|
without the operator's relayed decision is refused; on a server that reads mandates (`agent_id`) it also names its effect class, for example
|
|
271
294
|
`change-policy` (DEC-0125); the operator reaches it through an agent that relays their decision in `_meta`, the
|
|
272
|
-
service's page or its elicitation (Claude Code and Codex, which cannot set `_meta`, wait for CT-125's forwarding), and the service names it in its docs; otherwise a moved slot stays moved
|
|
273
|
-
|
|
274
|
-
surface.
|
|
295
|
+
service's page or its elicitation (Claude Code and Codex, which cannot set `_meta`, wait for CT-125's forwarding), and the service names it in its docs; otherwise a moved slot stays moved for good today (no Fabric release offers the operator's move back, above). The
|
|
296
|
+
hand-over of accepted requests to another session (contract CT-175) waits for Fabric's session surface.
|
|
275
297
|
|
|
276
298
|
## What a board session may call
|
|
277
299
|
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/kit-modules.md
CHANGED
|
@@ -87,7 +87,7 @@ A module imports only modules listed before it in `kit.json`, so the kit has no
|
|
|
87
87
|
| L4 | `fabric_hub` | the Fabric hub's door (`fabric-hub/0.1`) |
|
|
88
88
|
| L4 | `fabric_board_shapes` | the project board's wire: constants, refusal codes, shapes |
|
|
89
89
|
| L4 | `fabric_board_link` | the board's link: the MCP session over the hub, the fsynced checkpoint, a claimed request as data |
|
|
90
|
-
| L4 | `fabric_board_slots` | the responder's slot records: the generation held, a move
|
|
90
|
+
| L4 | `fabric_board_slots` | the responder's slot records: the generation held, a move, its take-back and a move back (FAA-165), one capability's refusal retried alone (FAA-163) |
|
|
91
91
|
| L4 | `fabric_board_session` | the board responder: the work context, the fenced request, transfer and recovery |
|
|
92
92
|
| L4 | `fabric_board_fixture` | the scripted board the probe runs a responder against |
|
|
93
93
|
| L4 | `fabric_board` | the facade of the board connector ([board](board.md)) |
|
|
@@ -75,9 +75,14 @@ see a swap made and undone between two of those checks. Pack from a folder only
|
|
|
75
75
|
Then every problem is listed, each with its `rule`: `schema` (the contract's schema), `FAC-SEM-064` (whole: its own
|
|
76
76
|
digest, each path once, every named file listed with the same SHA-256), `FAC-SEM-065` (no secret), `FAC-SEM-066`
|
|
77
77
|
(`agentRef` derived), `FAC-SEM-102` (one role, one server) and `folder` (`file-missing`, `file-digest`, `file-unlisted`,
|
|
78
|
-
`plugin-manifest`, `provider-manifest
|
|
78
|
+
`plugin-manifest`, `provider-manifest`, whose message names its two possible causes: a provider manifest that is not a JSON object,
|
|
79
|
+
or `costPresets` that differ from the package's, FAA-193) and `path` (a listed or folder path holding a control character, U+2028 or
|
|
79
80
|
U+2029, a `..` segment, a backslash, `//` or a colon, refused by the kit itself whatever the schema's pattern lets
|
|
80
|
-
through, FAA-78).
|
|
81
|
+
through, FAA-78). `fabric.package.json`, the trust file and a JWK key file take no `NaN` or `Infinity` token (a number too large for a double still reads as an infinity, in both kits as in `JSON.parse`), and the first two are read as strict UTF-8 and nested at most 256 levels; the plugin and
|
|
82
|
+
provider manifests the folder check reads are strict UTF-8 too (the contract's `checkPackageFolder` decodes them with
|
|
83
|
+
replacement characters, so an invalid byte inside a string value the check does not compare is no manifest here and is accepted there, FAA-194 (29)), take no `NaN` or `Infinity` token either, as `JSON.parse` takes none, and have no depth
|
|
84
|
+
limit of their own, as the contract's reference has none (Python 3.9's own recursion limit reads one nested about 990
|
|
85
|
+
levels deep as no manifest, FAA-194 (27)); a folder entry the kit cannot
|
|
81
86
|
read is refused as `Cannot read <path>`, and a link, FIFO or other non-regular file is never followed. With a problem,
|
|
82
87
|
`--apply` writes nothing. Repacking drops old signatures and
|
|
83
88
|
says so: a changed package is signed again.
|
|
@@ -88,6 +88,7 @@ export class Responder extends SlotKeeper {
|
|
|
88
88
|
this.waitUntil = 0;
|
|
89
89
|
this.stopping = false;
|
|
90
90
|
this.claimKeys = {};
|
|
91
|
+
this.probing = new Set(); // moved slots whose move-back claim lost its answer: asked again under the same key
|
|
91
92
|
this.said = {};
|
|
92
93
|
this.rr = 0;
|
|
93
94
|
this.reconcileAt = 0;
|
|
@@ -172,6 +173,7 @@ export class Responder extends SlotKeeper {
|
|
|
172
173
|
this.state = 'connecting';
|
|
173
174
|
this.slots = {}; // the previous session's slots are not this one's: a refusal now reconnects (FAA-163)
|
|
174
175
|
this.confirmed = {}; // nor its confirmations: a slot may have moved between the two sessions
|
|
176
|
+
this.probing = new Set(); // nor its lost move-back claims
|
|
175
177
|
const grant = await this.hub.joinBoard(this.project, this.credentialFile);
|
|
176
178
|
const missing = this.capabilities.filter((c) => !grant.capabilities.includes(c));
|
|
177
179
|
const session = this.sessionFactory(grant);
|
|
@@ -462,7 +464,9 @@ export class Responder extends SlotKeeper {
|
|
|
462
464
|
/** Seconds this session's leases still run, by its own clock: the running attempt's, else the oldest held slot's. */
|
|
463
465
|
leaseLeft() {
|
|
464
466
|
const renewed = this.current ? [this.current.renewedAt ?? 0]
|
|
465
|
-
: Object.values(this.slots).filter((s) => s.state === 'held' && s.renewedAt !== -Infinity).map((s) => s.renewedAt ?? 0)
|
|
467
|
+
: [...Object.values(this.slots).filter((s) => s.state === 'held' && s.renewedAt !== -Infinity).map((s) => s.renewedAt ?? 0), // a lapsed slot holds no lease (FAA-163)
|
|
468
|
+
...Object.entries(this.slots).filter(([c, s]) => this.probing.has(c) && 'probeAt' in s && this.now() - s.probeAt < LEASE_SECONDS - LEASE_MARGIN_SECONDS)
|
|
469
|
+
.map(([, s]) => s.probeAt)]; // a move-back claim whose answer was lost holds one from its send, while more than the 10 s margin of that lease is left (FAA-165)
|
|
466
470
|
return renewed.length && this.session ? Math.min(...renewed) + LEASE_SECONDS - this.now() : 0;
|
|
467
471
|
}
|
|
468
472
|
|
|
@@ -528,7 +532,10 @@ export class Responder extends SlotKeeper {
|
|
|
528
532
|
this.suspect.delete(capability);
|
|
529
533
|
}
|
|
530
534
|
for (const [capability, slot] of Object.entries(this.slots)) {
|
|
531
|
-
if (slot.state === 'moved')
|
|
535
|
+
if (slot.state === 'moved') { // never enrolled again on its own, even when the new holder lapses (FAA-155)
|
|
536
|
+
if (this.now() - (slot.renewedAt ?? 0) >= RENEW_SECONDS - 0.5) await this.movedBack(capability); // unless com.status shows a later generation it may hold (FAA-165)
|
|
537
|
+
continue;
|
|
538
|
+
}
|
|
532
539
|
if (slot.state === 'paused') {
|
|
533
540
|
// still held: a longer pause; lifted: the slot is ours again
|
|
534
541
|
if (capability !== heldByWork && this.now() >= (slot.retryAt ?? 0) && !(await this.gone(capability))) await this.enroll(capability); // a move is read first
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-slots.mjs
CHANGED
|
@@ -16,6 +16,8 @@ export const HOLD_RETRY_MAX_SECONDS = 900;
|
|
|
16
16
|
export const SESSION_CODES = Object.freeze(new Set(['unreachable', 'session-ended', 'not_authorized', 'authority_changed', 'credential', 'no-epoch',
|
|
17
17
|
'schema-unsupported', 'checkpoint', 'receipt-shape', 'idempotency_conflict', 'not-running', 'not-a-hub', 'hub-unavailable', 'protocol', 'not_available']));
|
|
18
18
|
|
|
19
|
+
const sameRecord = (a, b) => JSON.stringify(a ?? {}) === JSON.stringify(b ?? {});
|
|
20
|
+
|
|
19
21
|
/** The slot methods of Responder, which owns every field they read (checkpoint, slots, project, locked). */
|
|
20
22
|
export class SlotKeeper {
|
|
21
23
|
// -- a slot moved to another session (FAA-155, DEC-0140 PR-2) --
|
|
@@ -127,13 +129,88 @@ export class SlotKeeper {
|
|
|
127
129
|
this.toResume = this.toResume.filter((r) => this.checkpoint.entries[r]?.capability !== capability);
|
|
128
130
|
this.slots[capability] = { state: 'moved', generation: now, renewedAt: this.now() };
|
|
129
131
|
delete this.confirmed[capability];
|
|
132
|
+
delete this.claimKeys[capability]; // the move returned that claim to the queue: its receipt is dead
|
|
130
133
|
if (!('moved' in held)) { // once, even across restarts
|
|
131
|
-
this.emit('board.slot.moved', 'warning', `The ${capability} slot on this Project moved to another session. This service stopped answering it
|
|
134
|
+
this.emit('board.slot.moved', 'warning', `The ${capability} slot on this Project moved to another session. This service stopped answering it; accepted work stays \`accepted\` (work begun is the new holder's). The operator brings it back by take-back (once the other session lets it go, or at a restart with accepted work waiting; a restart alone does not) or, where Fabric offers it, a move back here. Else ask its maintainer.`, { notify: true, episode: capability, capability, generation: now, held: held.generation });
|
|
132
135
|
this.note(capability, { ...held, moved: now }); // said again at the next start if it cannot be written
|
|
133
136
|
}
|
|
134
137
|
return true;
|
|
135
138
|
}
|
|
136
139
|
|
|
140
|
+
/**
|
|
141
|
+
* A moved slot asked about once per later generation (FAA-165): the operator may move it back to this session
|
|
142
|
+
* (DEC-0140 PR-2), which this client cannot hear, so when com.status shows a generation past the recorded move, one
|
|
143
|
+
* com.claim asks whether the slot is its own. A claim that lands makes it held again and starts what it handed out; a
|
|
144
|
+
* lease_expired one (the move reached this session but its lease lapsed) enrolls again; a fenced one is another
|
|
145
|
+
* session's, recorded so the same generation is not asked about twice. Never an enroll or a replace on a slot another
|
|
146
|
+
* session holds: a slot moved off stays off unless it is handed back.
|
|
147
|
+
*/
|
|
148
|
+
async movedBack(capability) {
|
|
149
|
+
if (this.current || this.stopping) return; // a claim now could hand out a second request, or start work while the responder stops
|
|
150
|
+
const held = this.checkpoint.slots[capability] ?? {};
|
|
151
|
+
if (held.project !== this.project || !isInt(held.moved, 1)) return;
|
|
152
|
+
if (this.slots[capability]) this.slots[capability].renewedAt = this.now();
|
|
153
|
+
const now = ((await this.status()).responders ?? []).find((r) => isObj(r) && r.capability === capability)?.generation;
|
|
154
|
+
if (!isInt(now, 1) || now <= held.moved) return;
|
|
155
|
+
if (!this.probing.has(capability) || !this.claimKeys[capability]) { // a fresh key: one from before the move may hold a dead receipt
|
|
156
|
+
this.claimKeys[capability] = this.key('claim');
|
|
157
|
+
if (this.slots[capability]) delete this.slots[capability].probeAt; // and a send time of its own
|
|
158
|
+
}
|
|
159
|
+
let answer;
|
|
160
|
+
const sent = this.now();
|
|
161
|
+
try {
|
|
162
|
+
answer = this.receipt('com.claim', await this.call('com.claim', { idempotency: this.idem(this.claimKeys[capability]), capability }));
|
|
163
|
+
} catch (e) {
|
|
164
|
+
if (!(e instanceof BoardError)) throw e; // not the board's answer
|
|
165
|
+
if (e.code === 'unreachable') { // the claim may have landed: asked again under this key, never left to the fault
|
|
166
|
+
this.probing.add(capability);
|
|
167
|
+
if (this.slots[capability] && !('probeAt' in this.slots[capability])) this.slots[capability].probeAt = sent; // its lease runs from the first send
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
this.probing.delete(capability);
|
|
171
|
+
if (e.code === 'held') return; // the operator's hold: asked again at the next renewal
|
|
172
|
+
if (e.code === 'idempotency_window_expired' || e.code === 'capacity_exceeded') {
|
|
173
|
+
delete this.claimKeys[capability];
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
if (e.code === 'lease_expired') {
|
|
177
|
+
delete this.claimKeys[capability]; // a dead attempt's receipt may sit under it
|
|
178
|
+
if (await this.back(capability, held, now)) {
|
|
179
|
+
this.slots[capability].renewedAt = -Infinity; // lapsed: the enroll below is recorded first
|
|
180
|
+
await this.enroll(capability);
|
|
181
|
+
}
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
if (SESSION_CODES.has(e.code) && e.code !== 'not_authorized') throw e; // the board session's fault, not this slot's
|
|
185
|
+
// fenced, or another refusal of this claim: not this session's slot at this generation; a takeBack meanwhile wins (FAA-158)
|
|
186
|
+
await this.locked(() => { if (sameRecord(this.checkpoint.slots[capability], held)) this.note(capability, { ...held, moved: now }); });
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
this.probing.delete(capability);
|
|
190
|
+
const generation = isObj(answer.attempt) && isInt(answer.attempt.generation, 1) ? answer.attempt.generation : now;
|
|
191
|
+
if (generation < now) { // a replay from before a later move: dead, asked again fresh
|
|
192
|
+
delete this.claimKeys[capability];
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
if (!(await this.back(capability, held, generation))) return;
|
|
196
|
+
if ('request' in answer) {
|
|
197
|
+
delete this.claimKeys[capability];
|
|
198
|
+
this.slots[capability].renewedAt = this.now(); // the claim just landed: its lease runs (finding 38)
|
|
199
|
+
await this.start(capability, answer);
|
|
200
|
+
} else await this.enroll(capability); // renewed now: whether an empty com.claim renews the lease is not the contract's to say
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** The slot is this session's again at `generation`: held, its record rewritten without the move (FAA-165). */
|
|
204
|
+
back(capability, held, generation) {
|
|
205
|
+
return this.locked(() => {
|
|
206
|
+
if (!sameRecord(this.checkpoint.slots[capability], held)) return false; // a takeBack forgot it; its own next tick enrolls
|
|
207
|
+
this.slots[capability] = { state: 'held', generation, renewedAt: -Infinity }; // no lease yet
|
|
208
|
+
this.heldAt(capability, generation);
|
|
209
|
+
delete this.said['board.slot.moved']; // a later move is said again
|
|
210
|
+
return true;
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
137
214
|
/**
|
|
138
215
|
* The operator's act, never the kit's: forget that a capability's slot moved away. In the same board session the next
|
|
139
216
|
* tick enrolls once the slot is free; a reconnect with held work takes it back at once by a transfer (FAA-155).
|
|
@@ -141,6 +218,8 @@ export class SlotKeeper {
|
|
|
141
218
|
takeBack(capability) {
|
|
142
219
|
if (!this.capabilities.includes(capability)) throw new BoardError('invalid_arguments', `This responder does not answer ${capability}.`);
|
|
143
220
|
this.checkpoint.slot(capability, null);
|
|
221
|
+
delete this.claimKeys[capability]; // a receipt from before the take-back may be dead
|
|
222
|
+
this.probing.delete(capability);
|
|
144
223
|
if (this.slots[capability]?.state === 'moved') this.slots[capability] = { state: 'contested', renewedAt: 0 };
|
|
145
224
|
delete this.said['board.slot.moved'];
|
|
146
225
|
}
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-package.mjs
CHANGED
|
@@ -504,12 +504,17 @@ export function compose(draft, files) {
|
|
|
504
504
|
return { pkg, notes };
|
|
505
505
|
}
|
|
506
506
|
|
|
507
|
+
/** What a folder problem means, where its code alone does not say what to change (FAA-193). */
|
|
508
|
+
const FOLDER_MESSAGES = {
|
|
509
|
+
'provider-manifest': 'provider-manifest: the provider manifest must be a JSON object, and when it carries costPresets fabric.package.json must carry the same costPresets (copy them; pack does not)',
|
|
510
|
+
};
|
|
511
|
+
|
|
507
512
|
function allProblems(pkg, files) {
|
|
508
513
|
return [
|
|
509
514
|
...schemaProblems(pkg).map((p) => ({ ...p, rule: 'schema' })),
|
|
510
515
|
...pathProblems(pkg).map((p) => ({ ...p, rule: 'path' })),
|
|
511
516
|
...packageFindings(pkg).map((p) => ({ ...p, rule: p.code })),
|
|
512
|
-
...checkPackageFolder(pkg, files).map((p) => ({ rule: 'folder', problem: p.problem, instancePath: p.path, message: p.problem })),
|
|
517
|
+
...checkPackageFolder(pkg, files).map((p) => ({ rule: 'folder', problem: p.problem, instancePath: p.path, message: FOLDER_MESSAGES[p.problem] ?? p.problem })),
|
|
513
518
|
];
|
|
514
519
|
}
|
|
515
520
|
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_session.py
CHANGED
|
@@ -155,6 +155,7 @@ class Responder(SlotKeeper):
|
|
|
155
155
|
self.wait_until = 0.0
|
|
156
156
|
self.stopping = False
|
|
157
157
|
self.claim_keys: Dict[str, str] = {}
|
|
158
|
+
self._probing: set = set() # moved slots whose move-back claim lost its answer: asked again under the same key
|
|
158
159
|
self._said: Dict[str, str] = {}
|
|
159
160
|
self._rr = 0
|
|
160
161
|
#: The first pause under an operator's hold (DEC-0079); it doubles to HOLD_RETRY_MAX_SECONDS.
|
|
@@ -237,6 +238,7 @@ class Responder(SlotKeeper):
|
|
|
237
238
|
self.state = "connecting"
|
|
238
239
|
self.slots = {} # the previous session's slots are not this one's: a refusal now reconnects (FAA-163)
|
|
239
240
|
self._confirmed = {} # nor its confirmations: a slot may have moved between the two sessions
|
|
241
|
+
self._probing = set() # nor its lost move-back claims
|
|
240
242
|
grant = self.hub.join_board(self.project, self.credential_file)
|
|
241
243
|
missing = [c for c in self.capabilities if c not in grant.capabilities]
|
|
242
244
|
session = self.session_factory(grant)
|
|
@@ -516,7 +518,11 @@ class Responder(SlotKeeper):
|
|
|
516
518
|
"""Seconds this session's leases still run, by its own clock: the running attempt's, else the oldest held slot's."""
|
|
517
519
|
renewed = [self.current.renewed_at or 0.0] if self.current is not None else \
|
|
518
520
|
[s.get("renewed_at", 0.0) for s in self.slots.values() if s.get("state") == "held"
|
|
519
|
-
and s.get("renewed_at", 0.0) != float("-inf")]
|
|
521
|
+
and s.get("renewed_at", 0.0) != float("-inf")] + \
|
|
522
|
+
[s["probe_at"] for c, s in self.slots.items() if c in self._probing and "probe_at" in s
|
|
523
|
+
and self.now() - s["probe_at"] < LEASE_SECONDS - LEASE_MARGIN_SECONDS]
|
|
524
|
+
# a lapsed slot holds no lease (FAA-163); a move-back claim whose answer was lost holds one from its send, while
|
|
525
|
+
# more than the 10 s margin of that lease is left (FAA-165)
|
|
520
526
|
return min(renewed) + LEASE_SECONDS - self.now() if renewed and self.session is not None else 0.0
|
|
521
527
|
|
|
522
528
|
def _fault(self, exc: Exception) -> float:
|
|
@@ -588,8 +594,10 @@ class Responder(SlotKeeper):
|
|
|
588
594
|
self._gone(capability)
|
|
589
595
|
self._suspect.discard(capability)
|
|
590
596
|
for capability, slot in list(self.slots.items()):
|
|
591
|
-
if slot.get("state") == "moved":
|
|
592
|
-
|
|
597
|
+
if slot.get("state") == "moved": # never enrolled again on its own, even when the new holder lapses (FAA-155)
|
|
598
|
+
if self.now() - slot.get("renewed_at", 0) >= RENEW_SECONDS - 0.5:
|
|
599
|
+
self._moved_back(capability) # unless com.status shows a later generation it may hold (FAA-165)
|
|
600
|
+
continue
|
|
593
601
|
if slot.get("state") == "paused":
|
|
594
602
|
if capability != held_by_work and self.now() >= slot.get("retry_at", 0.0) and not self._gone(capability):
|
|
595
603
|
self._enroll(capability) # still held: a longer pause; lifted: the slot is ours again (moved: read first)
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_slots.py
CHANGED
|
@@ -120,22 +120,98 @@ class SlotKeeper:
|
|
|
120
120
|
self._to_resume = [r for r in self._to_resume if (self.checkpoint.entries.get(r) or {}).get("capability") != capability]
|
|
121
121
|
self.slots[capability] = {"state": "moved", "generation": now, "renewed_at": self.now()}
|
|
122
122
|
self._confirmed.pop(capability, None)
|
|
123
|
+
self.claim_keys.pop(capability, None) # the move returned that claim to the queue: its receipt is dead
|
|
123
124
|
if "moved" not in held:
|
|
124
125
|
self._note(capability, dict(held, moved=now)) # said again at the next start if it cannot be written
|
|
125
126
|
if "moved" not in held: # once, even across restarts
|
|
126
|
-
self._emit("board.slot.moved", "warning", "The %s slot on this Project moved to another session. This service stopped answering it
|
|
127
|
-
"
|
|
128
|
-
"
|
|
129
|
-
"
|
|
127
|
+
self._emit("board.slot.moved", "warning", "The %s slot on this Project moved to another session. This service stopped answering it; accepted "
|
|
128
|
+
"work stays `accepted` (work begun is the new holder's). The operator brings it back by take-back (once the "
|
|
129
|
+
"other session lets it go, or at a restart with accepted work waiting; a restart alone does not) or, where "
|
|
130
|
+
"Fabric offers it, a move back here. Else ask its maintainer." % capability,
|
|
130
131
|
notify=True, episode=capability, capability=capability, generation=now, held=held["generation"])
|
|
131
132
|
return True
|
|
132
133
|
|
|
134
|
+
def _moved_back(self, capability: str) -> None:
|
|
135
|
+
"""A moved slot asked about once per later generation (FAA-165): the operator may move it back to this session
|
|
136
|
+
(DEC-0140 PR-2), which this client cannot hear, so when com.status shows a generation past the recorded move, one
|
|
137
|
+
`com.claim` asks whether the slot is its own. A claim that lands makes it held again and starts what it handed
|
|
138
|
+
out; a `lease_expired` one (the move reached this session but its lease lapsed) enrolls again; a fenced one is
|
|
139
|
+
another session's, recorded so the same generation is not asked about twice. Never an enroll or a replace on a
|
|
140
|
+
slot another session holds: a slot moved off stays off unless it is handed back."""
|
|
141
|
+
if self.current is not None or self.stopping:
|
|
142
|
+
return # a claim now could hand out a second request, or start work while the responder stops
|
|
143
|
+
held = self.checkpoint.slots.get(capability) or {}
|
|
144
|
+
seen = held.get("moved")
|
|
145
|
+
if held.get("project") != self.project or not _is_int(seen, 1):
|
|
146
|
+
return
|
|
147
|
+
(self.slots.get(capability) or {})["renewed_at"] = self.now()
|
|
148
|
+
status = self._status()
|
|
149
|
+
now = _whole(next((r.get("generation") for r in status.get("responders") or [] if isinstance(r, dict) and r.get("capability") == capability), None))
|
|
150
|
+
if not _is_int(now, 1) or now <= seen:
|
|
151
|
+
return
|
|
152
|
+
key = self.claim_keys.get(capability) if capability in self._probing else None # read once: take_back may pop it
|
|
153
|
+
if key is None:
|
|
154
|
+
key = self.claim_keys[capability] = self._key("claim") # a fresh key: one from before the move may hold a dead receipt
|
|
155
|
+
(self.slots.get(capability) or {}).pop("probe_at", None) # and a send time of its own
|
|
156
|
+
sent = self.now()
|
|
157
|
+
try:
|
|
158
|
+
answer = self._receipt("com.claim", self._call("com.claim", {"idempotency": self._idem(key), "capability": capability}))
|
|
159
|
+
except BoardError as exc:
|
|
160
|
+
if exc.code == "unreachable": # the claim may have landed: asked again under this key, never left to the fault
|
|
161
|
+
self._probing.add(capability)
|
|
162
|
+
(self.slots.get(capability) or {}).setdefault("probe_at", sent) # its lease runs from the first send
|
|
163
|
+
return
|
|
164
|
+
self._probing.discard(capability)
|
|
165
|
+
if exc.code == "held":
|
|
166
|
+
return # the operator's hold: asked again at the next renewal
|
|
167
|
+
if exc.code in ("idempotency_window_expired", "capacity_exceeded"):
|
|
168
|
+
self.claim_keys.pop(capability, None)
|
|
169
|
+
return
|
|
170
|
+
if exc.code == "lease_expired":
|
|
171
|
+
self.claim_keys.pop(capability, None) # a dead attempt's receipt may sit under it
|
|
172
|
+
if self._back(capability, held, now):
|
|
173
|
+
self.slots[capability]["renewed_at"] = float("-inf") # lapsed: the enroll below is recorded first
|
|
174
|
+
self._enroll(capability)
|
|
175
|
+
return
|
|
176
|
+
if exc.code in SESSION_CODES and exc.code != "not_authorized":
|
|
177
|
+
raise # the board session's fault, not this slot's
|
|
178
|
+
with self._lock: # fenced, or another refusal of this claim: not this session's slot at this generation
|
|
179
|
+
if self.checkpoint.slots.get(capability) == held: # take_back meanwhile wins (FAA-158)
|
|
180
|
+
self._note(capability, dict(held, moved=now))
|
|
181
|
+
return
|
|
182
|
+
self._probing.discard(capability)
|
|
183
|
+
attempt = answer.get("attempt") if isinstance(answer.get("attempt"), dict) else {}
|
|
184
|
+
generation = _whole(attempt.get("generation"))
|
|
185
|
+
if _is_int(generation, 1) and generation < now: # a replay from before a later move: dead, asked again fresh
|
|
186
|
+
self.claim_keys.pop(capability, None)
|
|
187
|
+
return
|
|
188
|
+
if not self._back(capability, held, generation if _is_int(generation, 1) else now):
|
|
189
|
+
return
|
|
190
|
+
if "request" in answer:
|
|
191
|
+
self.claim_keys.pop(capability, None)
|
|
192
|
+
self.slots[capability]["renewed_at"] = self.now() # the claim just landed: its lease runs (finding 38)
|
|
193
|
+
self._start(capability, answer)
|
|
194
|
+
else:
|
|
195
|
+
self._enroll(capability) # renewed now: whether an empty com.claim renews the lease is not the contract's to say
|
|
196
|
+
|
|
197
|
+
def _back(self, capability: str, held: Dict[str, Any], generation: int) -> bool:
|
|
198
|
+
"""The slot is this session's again at `generation`: held, its record rewritten without the move (FAA-165)."""
|
|
199
|
+
with self._lock:
|
|
200
|
+
if self.checkpoint.slots.get(capability) != held:
|
|
201
|
+
return False # take_back (an action thread) forgot the record meanwhile; its own next tick enrolls
|
|
202
|
+
self.slots[capability] = {"state": "held", "generation": generation, "renewed_at": float("-inf")} # no lease yet
|
|
203
|
+
self._held_at(capability, generation)
|
|
204
|
+
self._said.pop("board.slot.moved", None) # a later move is said again
|
|
205
|
+
return True
|
|
206
|
+
|
|
133
207
|
def take_back(self, capability: str) -> None:
|
|
134
208
|
"""The operator's act, never the kit's: forget the move; the next tick enrolls once the slot is free (FAA-155)."""
|
|
135
209
|
if capability not in self.capabilities:
|
|
136
210
|
raise BoardError("invalid_arguments", "This responder does not answer %s." % capability)
|
|
137
211
|
with self._lock:
|
|
138
212
|
self.checkpoint.slot(capability, None)
|
|
213
|
+
self.claim_keys.pop(capability, None) # a receipt from before the take-back may be dead
|
|
214
|
+
self._probing.discard(capability)
|
|
139
215
|
if (self.slots.get(capability) or {}).get("state") == "moved":
|
|
140
216
|
self.slots[capability] = {"state": "contested", "renewed_at": 0.0}
|
|
141
217
|
self._said.pop("board.slot.moved", None)
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_package.py
CHANGED
|
@@ -133,7 +133,7 @@ def read_signing_key(path: Any, kid: Optional[str] = None) -> Tuple[bytes, str]:
|
|
|
133
133
|
text = raw.decode("utf-8", "replace").strip(JS_WHITESPACE) # as JavaScript's trim: a leading BOM goes too
|
|
134
134
|
if text.startswith("{"):
|
|
135
135
|
try:
|
|
136
|
-
jwk = json.loads(text)
|
|
136
|
+
jwk = json.loads(text, parse_constant=fd._no_constant) # NaN and Infinity are no JSON, as JSON.parse says
|
|
137
137
|
except ValueError:
|
|
138
138
|
raise PackageError("The key file is neither a JWK nor a PEM private key.") from None
|
|
139
139
|
if not isinstance(jwk, dict) or jwk.get("kty") != "OKP" or jwk.get("crv") != "Ed25519" or not isinstance(jwk.get("d"), str):
|
|
@@ -394,8 +394,8 @@ def _parsed(data: Optional[bytes]) -> Optional[Dict[str, Any]]:
|
|
|
394
394
|
if data is None:
|
|
395
395
|
return None
|
|
396
396
|
try:
|
|
397
|
-
value = json.loads(data.decode("utf-8"))
|
|
398
|
-
except (ValueError, RecursionError): # invalid UTF-8 is no manifest, in both kits
|
|
397
|
+
value = json.loads(data.decode("utf-8"), parse_constant=fd._no_constant)
|
|
398
|
+
except (ValueError, RecursionError): # invalid UTF-8, NaN or Infinity is no manifest, in both kits
|
|
399
399
|
return None
|
|
400
400
|
return value if isinstance(value, dict) else None
|
|
401
401
|
|
|
@@ -424,7 +424,7 @@ def check_package_folder(pkg: Dict[str, Any], files: Dict[str, bytes]) -> List[D
|
|
|
424
424
|
provider = pkg.get("provider")
|
|
425
425
|
if isinstance(provider, str) and provider in files:
|
|
426
426
|
manifest = _parsed(files.get(provider))
|
|
427
|
-
if
|
|
427
|
+
if manifest is None or ("costPresets" in manifest and not ("costPresets" in pkg and _json_equal(manifest["costPresets"], pkg["costPresets"]))):
|
|
428
428
|
problems.append({"problem": "provider-manifest", "path": provider})
|
|
429
429
|
return problems
|
|
430
430
|
|
|
@@ -648,7 +648,7 @@ def _load_json(data: bytes, what: str) -> Dict[str, Any]:
|
|
|
648
648
|
if _nesting(text) > MAX_NESTING:
|
|
649
649
|
raise PackageError("%s nests deeper than %d levels." % (what, MAX_NESTING))
|
|
650
650
|
try:
|
|
651
|
-
value = json.loads(text, object_pairs_hook=fd._no_duplicates)
|
|
651
|
+
value = json.loads(text, object_pairs_hook=fd._no_duplicates, parse_constant=fd._no_constant)
|
|
652
652
|
except fd._Duplicate as exc:
|
|
653
653
|
raise PackageError("%s names %s twice in one object." % (what, json.dumps(exc.args[0], ensure_ascii=False))) from None
|
|
654
654
|
except (ValueError, RecursionError) as exc:
|
|
@@ -704,10 +704,18 @@ def _all_problems(pkg: Dict[str, Any], files: Dict[str, bytes]) -> List[Dict[str
|
|
|
704
704
|
problems = [dict(p, rule="schema") for p in schema_problems(pkg)]
|
|
705
705
|
problems += [dict(p, rule="path") for p in path_problems(pkg)]
|
|
706
706
|
problems += [dict(p, rule=p["code"]) for p in package_findings(pkg)]
|
|
707
|
-
problems += [{"rule": "folder", "problem": p["problem"], "instancePath": p["path"], "message": p["problem"]
|
|
707
|
+
problems += [{"rule": "folder", "problem": p["problem"], "instancePath": p["path"], "message": FOLDER_MESSAGES.get(p["problem"], p["problem"])}
|
|
708
|
+
for p in check_package_folder(pkg, files)]
|
|
708
709
|
return problems
|
|
709
710
|
|
|
710
711
|
|
|
712
|
+
#: What a folder problem means, where its code alone does not say what to change (FAA-193).
|
|
713
|
+
FOLDER_MESSAGES = {
|
|
714
|
+
"provider-manifest": "provider-manifest: the provider manifest must be a JSON object, and when it carries costPresets "
|
|
715
|
+
"fabric.package.json must carry the same costPresets (copy them; pack does not)",
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
|
|
711
719
|
def pack(folder: Any, apply: bool = False) -> Dict[str, Any]:
|
|
712
720
|
"""Pack ``folder`` (see the module's doc). The answer names the digest, ``agentRef`` and every problem; with
|
|
713
721
|
``apply`` and no problem, ``fabric.package.json`` is rewritten."""
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fabric-service-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.60.0",
|
|
4
4
|
"about": "The service kit's modules in dependency order: a module imports only modules listed before it. Written by fabric_kit.py build; edit the order and layers by hand, never the digests.",
|
|
5
5
|
"python": [
|
|
6
6
|
{
|
|
@@ -165,7 +165,7 @@
|
|
|
165
165
|
"fabric_package_schema",
|
|
166
166
|
"fabric_secrets"
|
|
167
167
|
],
|
|
168
|
-
"sha256": "
|
|
168
|
+
"sha256": "1f2b9e73b2b32a7e1e887d4b69f87c6e21b4d1227ad924a235334223e6dd51ca"
|
|
169
169
|
},
|
|
170
170
|
{
|
|
171
171
|
"module": "fabric_protocols",
|
|
@@ -435,7 +435,7 @@
|
|
|
435
435
|
"imports": [
|
|
436
436
|
"fabric_board_shapes"
|
|
437
437
|
],
|
|
438
|
-
"sha256": "
|
|
438
|
+
"sha256": "92b065e3df935547cf46653245f1129e4a1f8b59b0a41e8fa9ffdc41596ee592"
|
|
439
439
|
},
|
|
440
440
|
{
|
|
441
441
|
"module": "fabric_board_session",
|
|
@@ -448,7 +448,7 @@
|
|
|
448
448
|
"fabric_hub",
|
|
449
449
|
"fabric_service"
|
|
450
450
|
],
|
|
451
|
-
"sha256": "
|
|
451
|
+
"sha256": "9b299b578ce8b1e834afdbbdb54183281df064302417fe364067e97e9ac23979"
|
|
452
452
|
},
|
|
453
453
|
{
|
|
454
454
|
"module": "fabric_board_fixture",
|
|
@@ -652,7 +652,7 @@
|
|
|
652
652
|
"fabric-package-schema",
|
|
653
653
|
"fabric-secrets"
|
|
654
654
|
],
|
|
655
|
-
"sha256": "
|
|
655
|
+
"sha256": "8cfb32eebbd4e1bcf28a6e4437344ae2dd54479b0e7c820f002e9f94ab9ecaad"
|
|
656
656
|
},
|
|
657
657
|
{
|
|
658
658
|
"module": "fabric-protocols",
|
|
@@ -917,7 +917,7 @@
|
|
|
917
917
|
"imports": [
|
|
918
918
|
"fabric-board-shapes"
|
|
919
919
|
],
|
|
920
|
-
"sha256": "
|
|
920
|
+
"sha256": "fc77a3c6e62aa18575568bef4f703cce2edaba8cef648813ebcdbb21bdc27a11"
|
|
921
921
|
},
|
|
922
922
|
{
|
|
923
923
|
"module": "fabric-board-session",
|
|
@@ -930,7 +930,7 @@
|
|
|
930
930
|
"fabric-hub",
|
|
931
931
|
"fabric-service"
|
|
932
932
|
],
|
|
933
|
-
"sha256": "
|
|
933
|
+
"sha256": "b81e74397fbae655183fc0dabb8d7406c9c5e9b6e02d893e046aa3521a3a8619"
|
|
934
934
|
},
|
|
935
935
|
{
|
|
936
936
|
"module": "fabric-board-fixture",
|
|
@@ -4498,6 +4498,96 @@
|
|
|
4498
4498
|
"fabric-openrouter.mjs": "6041f3ab30f516658e1dbab4897a651cc8509b2136cfa8e769802c6b40ec2f35",
|
|
4499
4499
|
"fabric-package.schema.json": "2ba799f576fad0f608029223c4ccccd122152f1c5dbfa8ce8b31cd63ac92ad44",
|
|
4500
4500
|
"share-registration.schema.json": "34b3997eeaf936ebc861342a23045634a7c18c19f01d7ed4198310d81989f5b0"
|
|
4501
|
+
},
|
|
4502
|
+
"0.60.0": {
|
|
4503
|
+
"fabric_jsvalue.py": "12888a2c934a885d35999ac9782eaedc8d04946d9775213344a235a8814d3d28",
|
|
4504
|
+
"fabric_time.py": "588f383da4ac6a243ea95b6ffc3a0aa99ca94245732dc4937734f190b7af4ae9",
|
|
4505
|
+
"fabric_identifiers.py": "0fd814d521ac3aa39907458265870753046a73f48af3d8581ab864b93b211607",
|
|
4506
|
+
"fabric_secrets.py": "1d0191f0ec108fdc68345056b8c574b1f691509d1c353e41d98bfd58a59e8d3c",
|
|
4507
|
+
"fabric_crypto.py": "9c34aee20dbe4d6742e4fdbaab68ceb934bbe7dbd3a904c0010824ace16cdeaa",
|
|
4508
|
+
"fabric_base.py": "f62bcc2ed9912660fa875d27a09f60867260cc4925f79873501fd9620be59611",
|
|
4509
|
+
"fabric_platform.py": "a9350dff55ef0fb3a23cdb4d0320a15868b15a8deb10d2eb74d72c58740c284b",
|
|
4510
|
+
"fabric_paths.py": "8a1ee742f80f436e9f28c61b4f049a9545de2a9e0d66bfc61faa6ee5d1ba3734",
|
|
4511
|
+
"fabric_estate_rules.py": "8b36a7f4108d9c2b1fb38c8bff344778c807bed631a99b4a563ecb0a49c10b1c",
|
|
4512
|
+
"fabric_estate.py": "b3cd56dcb86e04d252980322ac8054da1aaef24ac64690f7aa80e7c765ee6f60",
|
|
4513
|
+
"fabric_mandate.py": "050c43d0166a7dfba82e29c4e978f05c364cf6da06d0a781bd023a566777c17b",
|
|
4514
|
+
"fabric_delegation.py": "716f0ff7ac01095f54b24970beac1962128e8ff33026b243fc0467876142ab03",
|
|
4515
|
+
"fabric_share.py": "da36a466426fc29dd7dd40013167f3959a18375f8fedd228bf64f298cec1040c",
|
|
4516
|
+
"fabric_ceremonies.py": "a59e83cf4adb2669a9b98008e3d3a0cba991a35cbff97bc604fc82fe81cfd49f",
|
|
4517
|
+
"fabric_package_schema.py": "e623f0408d38e3343ed66222fb812907c6c52e1a286eb1c1176ab2e64640c9ad",
|
|
4518
|
+
"fabric_package.py": "1f2b9e73b2b32a7e1e887d4b69f87c6e21b4d1227ad924a235334223e6dd51ca",
|
|
4519
|
+
"fabric_protocols.py": "42529a7df4d753544d2e37a18ec81266d6e1e80477cb8558a3ab521416d45791",
|
|
4520
|
+
"fabric_actions.py": "ae21d1199614c4f74d1dcd1e0a87107a9d52434cd9e2d64aa24a7b1e196fb723",
|
|
4521
|
+
"fabric_connections.py": "7de55cb7d808f756d650effabfaa12027d40a5bd5d2f6d448c32969d47529ecb",
|
|
4522
|
+
"fabric_a2a.py": "8b01502131215bb4de1c01866187c56e99b791543923ed8577535573db33b919",
|
|
4523
|
+
"fabric_page_links.py": "1b57ea573c550e2f90d1ec3edcc655917ceaa0db9dd1635fccf422ec3024d25e",
|
|
4524
|
+
"fabric_lock.py": "ba2399d2137c7a7791b7133b9db9cca36693f4eb7e30955b25a75ade814e95ba",
|
|
4525
|
+
"fabric_usage.py": "5cc2e03ccc5b06e81987ed7bc3eead7a7e6cbf1a7cb8e05c6c64315eed9d91b6",
|
|
4526
|
+
"fabric_backups.py": "1f3f0d7ecf361b29e9101e07f1c53e5dc304d598bf26fd59ec89f16b9fed2420",
|
|
4527
|
+
"fabric_descriptor.py": "a6d7a4a38186f7ef527d16218bbf96e39e255f9547be668adcfbec063dabb7ed",
|
|
4528
|
+
"fabric_context.py": "6ae311ddc7217a4aff915e7ce2646486931acd128462b7bebe4a52bbb5fbb6c7",
|
|
4529
|
+
"fabric_auth.py": "71115d18c67b7b806cb57b64b77e01ad97fc0e63e6f49ee850f778123617d905",
|
|
4530
|
+
"fabric_events.py": "cd94b186e2a8f8cb4ad2b6bd44d83ec5021933075e28c5d655f387f16dff42b8",
|
|
4531
|
+
"fabric_logs.py": "86dc55bfafa2a57264722e2403510a249b95de0e4a722b2e4bc3948e3bd93da9",
|
|
4532
|
+
"fabric_supervisors.py": "3c5b2e8c310738720b1c1b779be4710e90251013d9be4c68f9952edc0bf55d5f",
|
|
4533
|
+
"fabric_service.py": "a2e813469a47d4d3eea996628635c8903007498bdb6a1ae032a7a5ce6f49fe2f",
|
|
4534
|
+
"fabric_profiles.py": "26e21ec29c016000f8ca8053b7b489184854a73be1b7fdc0a18d12990203db9b",
|
|
4535
|
+
"fabric_interop_rules.py": "82c079a4d9421ea2cd10c1dfa8e1c447b80d5e0dcc8d52d52415bf028e34c91e",
|
|
4536
|
+
"fabric_interop.py": "8f7e43bf01fa5e969d7d819f4b0188b9acfd846f6f85a37ea139e550d61e4e6c",
|
|
4537
|
+
"fabric_hub.py": "b9ac852e601b1d54cf61351677a628afc69faeed7d1fc74ebe541c1cf38e0c2a",
|
|
4538
|
+
"fabric_board_shapes.py": "0a0e8d3d40e33dfd8c457aedb85330b2ff25f8c49a33eb6cbd5a5cc8a5007f7e",
|
|
4539
|
+
"fabric_board_link.py": "e8d2310e5f53c75e4bf642c3d980b2faba692731eaf56ec245cea05446e38bef",
|
|
4540
|
+
"fabric_board_slots.py": "92b065e3df935547cf46653245f1129e4a1f8b59b0a41e8fa9ffdc41596ee592",
|
|
4541
|
+
"fabric_board_session.py": "9b299b578ce8b1e834afdbbdb54183281df064302417fe364067e97e9ac23979",
|
|
4542
|
+
"fabric_board_fixture.py": "69761ce205ec640762ec9b591c6903e5a87f844e8d423b041681924027e39d1e",
|
|
4543
|
+
"fabric_board.py": "34c1200860040aa61cdd35f911f70d34d64b4a5cd91f892328d1169e4a89d8cd",
|
|
4544
|
+
"fabric_http.py": "80e22ae0bba91de13a94d64b595108b39c58f16f7b4137f3a48bdd9503f424fe",
|
|
4545
|
+
"fabric_openrouter.py": "d06a40f83f2fb8bd441ab3485ae882f1fbdd9cd7a9c1ec175eb47cb8ae2cce81",
|
|
4546
|
+
"fabric-jsvalue.mjs": "dfbdb8c41584972c9389750a41719374a1cfc5652ed438dd1623025190b0106e",
|
|
4547
|
+
"fabric-time.mjs": "3ff932069038a720df1c68c6492976906737b2e67e5c12d9c81c418597f661de",
|
|
4548
|
+
"fabric-identifiers.mjs": "a18c2a501a4aa170bfd3d99e571ad6b7662649b4d3b9d2a540c002ea42811bbf",
|
|
4549
|
+
"fabric-secrets.mjs": "6ad8ab18ac899ecc2384e8ec6a3fb689b92be3b9a5ce3a59b92a37d52da5d766",
|
|
4550
|
+
"fabric-crypto.mjs": "038e03689701e3c9721a1e7f8e0a4d02a52d6a5d1661b0dc84035edc3ebbfcf6",
|
|
4551
|
+
"fabric-base.mjs": "7804b2df7715dbbf9702a9b61b6b8cef2110f9a52b04cabd8fb0a66458d87512",
|
|
4552
|
+
"fabric-platform.mjs": "a8119d95ebba4d589a9e401f5d6fc8dda62d59d44e4c1ff811d97fac3771ac0c",
|
|
4553
|
+
"fabric-paths.mjs": "aa630d611e748a899e7043480f4a4e46b444f2e15ff945f30c2fbef21c1ceea5",
|
|
4554
|
+
"fabric-estate-rules.mjs": "7476be71174aa5b1026134f2b335abf647feaa55584e2f9de8618e3054aa8e1e",
|
|
4555
|
+
"fabric-estate.mjs": "e8a2e62d2313121e6cb502f6e6abad94d1d546a44b4d9b3ce2be29e7808e8cf3",
|
|
4556
|
+
"fabric-mandate.mjs": "deaab2bfb929d1bd35ae1bad71e7b9ad9902cc23b8eb51819ed592fd80eddf95",
|
|
4557
|
+
"fabric-delegation.mjs": "04eaf6cd1786d4309a253a27dbdb05e27b408cb511aa41b1633a96634b775e65",
|
|
4558
|
+
"fabric-share.mjs": "bfb26581c21ad984c2a337da02429f9f3a165cc582f8ba48de4d3dfb7d163a59",
|
|
4559
|
+
"fabric-ceremonies.mjs": "117d4374c9e95c930a38de7adbd4323774798f4fe6ec6f24afcd6f2c7298ca44",
|
|
4560
|
+
"fabric-package-schema.mjs": "cd28092375db0fc21ffab1f0bb7794f7f8979ee08c64d3b9faf4b3845357caa6",
|
|
4561
|
+
"fabric-package.mjs": "8cfb32eebbd4e1bcf28a6e4437344ae2dd54479b0e7c820f002e9f94ab9ecaad",
|
|
4562
|
+
"fabric-protocols.mjs": "5fd8765fdd3e507cc85c108492682bf5cbb4ae08873245c50374fb01d33c75e4",
|
|
4563
|
+
"fabric-actions.mjs": "06e42f5e24c96311d5417d0d1effa93479ef7128a7ab139903b2bcf59253f6be",
|
|
4564
|
+
"fabric-connections.mjs": "303432e547ce563e90ca9ffcd827f5612f51d3582929834e1add0fdaf6485dcc",
|
|
4565
|
+
"fabric-a2a.mjs": "51b72fe1d80649d659fcad3a8f96370db3476c4a7b30cbabd8522c138be69346",
|
|
4566
|
+
"fabric-page-links.mjs": "a8834926e9d4bbf7c4857f551451d2ebf6ac8873bbccaaf89c5e30cf45b17ee6",
|
|
4567
|
+
"fabric-lock.mjs": "ac219ab0a326d5c43c7c4c255e0c3843d4794de795a60e3af661e7d5350ea052",
|
|
4568
|
+
"fabric-usage.mjs": "1c4d18004639409624ac3516a02f2b60bbf55abdb612e9a96c37e83666eb87a7",
|
|
4569
|
+
"fabric-backups.mjs": "cfd8c9085e3d597a783099c2bd2ffe2f3faa0daaadab46dedd30523894cfeb06",
|
|
4570
|
+
"fabric-descriptor.mjs": "243bbce42989f67c4c5ec6b7659e201b3f9a1c08c71d429d88ec934d870f5104",
|
|
4571
|
+
"fabric-context.mjs": "1112d695e872f9825cbc94f08484974bff3880087bc29b8be59f885f03e8f7be",
|
|
4572
|
+
"fabric-auth.mjs": "782c2aa732f8320f604194660117640a7ea145e12b8155c04e88cef7c516a5b3",
|
|
4573
|
+
"fabric-events.mjs": "c463d6ffcae24229a5c608d8fcea4268d2ccc5024b19fa10e5a452284068fa71",
|
|
4574
|
+
"fabric-logs.mjs": "58467e5719c16e099d05bbedec47a95712506ab03c33f3cd4be1252e8498a946",
|
|
4575
|
+
"fabric-supervisors.mjs": "d45c38de0cb3ed85e083496c2d3c51dab004454accdd87f4762a512a6d442573",
|
|
4576
|
+
"fabric-service.mjs": "6aa673657427211b491a9b63f94b9847a7e6aa3ee1b7353e1af11f46469803a3",
|
|
4577
|
+
"fabric-profiles.mjs": "314376cf2deb87f235fb18b11d3b532e211f585b9af0a9302b5f4cc39971b532",
|
|
4578
|
+
"fabric-interop-rules.mjs": "e4f8b133138e27353a7976c0360bd1653c90daed43b3f843f85a19f14f5e9221",
|
|
4579
|
+
"fabric-interop.mjs": "ee7e6d32be1f42e6d21a943b8886652352408c49c6404d9749b8a02dc3901e92",
|
|
4580
|
+
"fabric-hub.mjs": "c005115a864cfd67daa4862097bb14acfe5a60bf653897e5bf8a570dd80f1070",
|
|
4581
|
+
"fabric-board-shapes.mjs": "c1db7b7bd0d58d82bb6d8dfbc3cd1547a256a333052d367e82ff1b3edf2804d7",
|
|
4582
|
+
"fabric-board-link.mjs": "b1ac329b65b679193b94b81fe735922e14427ed540ba910b1a815ce3447e3220",
|
|
4583
|
+
"fabric-board-slots.mjs": "fc77a3c6e62aa18575568bef4f703cce2edaba8cef648813ebcdbb21bdc27a11",
|
|
4584
|
+
"fabric-board-session.mjs": "b81e74397fbae655183fc0dabb8d7406c9c5e9b6e02d893e046aa3521a3a8619",
|
|
4585
|
+
"fabric-board-fixture.mjs": "83c4b7e33ee47b68385faf2569d38d5c8a0c5cb0f8656090f41e1b0eed2f436e",
|
|
4586
|
+
"fabric-board.mjs": "0d75c3e27a0b791bdbc35b2e93b3299e5831d3ab3003ce441e6268397e32b448",
|
|
4587
|
+
"fabric-http.mjs": "9f899829073470b9c2b449605a9371b773848eec4e3a0122ed4ccde5eddcdb4b",
|
|
4588
|
+
"fabric-openrouter.mjs": "6041f3ab30f516658e1dbab4897a651cc8509b2136cfa8e769802c6b40ec2f35",
|
|
4589
|
+
"fabric-package.schema.json": "2ba799f576fad0f608029223c4ccccd122152f1c5dbfa8ce8b31cd63ac92ad44",
|
|
4590
|
+
"share-registration.schema.json": "34b3997eeaf936ebc861342a23045634a7c18c19f01d7ed4198310d81989f5b0"
|
|
4501
4591
|
}
|
|
4502
4592
|
},
|
|
4503
4593
|
"data": {
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/sample_service.py
CHANGED
|
@@ -100,7 +100,7 @@ CAPABILITIES = [
|
|
|
100
100
|
#: manifest, so `register --board-project` writes it there.
|
|
101
101
|
TAKE_BACK_CAPABILITY: Dict[str, Any] = {
|
|
102
102
|
"name": TAKE_BACK["tool"], "effect": "change-policy", "idempotency": "required", "job": False,
|
|
103
|
-
"description": "Take back a board slot
|
|
103
|
+
"description": "Take back a board slot moved to another session.",
|
|
104
104
|
# A relayed action: a probe call carries no operator decision, so it is refused and changes nothing (unit r28).
|
|
105
105
|
"probeAssertion": "is refused as operator-decision-required without the operator's decision in _meta",
|
|
106
106
|
"input": _schema("board.take-back-input", TAKE_BACK_INPUT),
|
|
@@ -175,7 +175,7 @@ class Service:
|
|
|
175
175
|
|
|
176
176
|
# --- the project board (opt-in): the input gate and the answer -------------------
|
|
177
177
|
def board_moved(self) -> list:
|
|
178
|
-
"""The capabilities whose slot
|
|
178
|
+
"""The capabilities whose slot moved to another session (FAA-155): the page offers their take-back.
|
|
179
179
|
The live slot or the checkpoint's record says so: before a restart has reconnected (or while Fabric is down) the
|
|
180
180
|
record alone knows the move (0.56.0 seam r3)."""
|
|
181
181
|
if not self.board:
|
|
@@ -5,7 +5,7 @@ license: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
|
|
|
5
5
|
compatibility: Requires filesystem access and Python 3.9+. Exact schema checks additionally need git, Node.js, pnpm, and the pinned fabric-agent-contract checkout (a public repository). Works without those tools in an explicitly degraded structural-check mode. Ships in one plugin with adapting-projects-to-fabric, whose scripts it reuses.
|
|
6
6
|
metadata:
|
|
7
7
|
author: PassionCode.ai
|
|
8
|
-
version: "0.
|
|
8
|
+
version: "0.60.0"
|
|
9
9
|
contract-version: "0.1.0"
|
|
10
10
|
contract-commit: "3830a3353923e20f826d466610e1c479f120bec0"
|
|
11
11
|
---
|
|
@@ -18,7 +18,7 @@ compatibility: >-
|
|
|
18
18
|
this plugin; Codex/Kimi use command hooks; Hermes/OpenCode/OpenClaw/Pi use native context plugins after opt-in.
|
|
19
19
|
metadata:
|
|
20
20
|
author: PassionCode.ai
|
|
21
|
-
version: "0.
|
|
21
|
+
version: "0.60.0"
|
|
22
22
|
---
|
|
23
23
|
|
|
24
24
|
# Working with Fabric
|
|
@@ -283,15 +283,15 @@ build or test in flight is not a stall. Note each restart (what stopped, when, a
|
|
|
283
283
|
|
|
284
284
|
## Step 6: tasks link to backlog rows, never copy them
|
|
285
285
|
|
|
286
|
-
A backlog row
|
|
286
|
+
A repository's backlog row stays the status source. File the Fabric task that runs it with the row's address:
|
|
287
287
|
|
|
288
288
|
```json
|
|
289
289
|
{"title": "…", "source": {"repository": "passioncode-ai/fabric-agent-adapter", "path": "docs/backlog.md", "id": "FAA-18"}}
|
|
290
290
|
```
|
|
291
291
|
|
|
292
|
-
through `fabric_task_create` (or `fabric_work_claim`, which files one when none exists)
|
|
293
|
-
|
|
294
|
-
|
|
292
|
+
through `fabric_task_create` (or `fabric_work_claim`, which files one when none exists); read its answer as Step 4
|
|
293
|
+
item 2 says. Your single live claim links the row (Fabric 0.3.14): file an unrelated row after your release, never linked
|
|
294
|
+
by hand, or say it is linked. Do not paste the row's text or status into the task; edit it in its repository.
|
|
295
295
|
|
|
296
296
|
## Step 7: at the end
|
|
297
297
|
|
|
@@ -61,12 +61,12 @@ reply or progress note): the target replied, not that the work was done. Read th
|
|
|
61
61
|
still cancel the request. `noResponder` in your `com.submit` receipt says nobody can answer that capability for that
|
|
62
62
|
Project now: the request is stored, a reply may still answer it and a responder may come later. An `accepted`
|
|
63
63
|
request can also wait unanswered for its responder's operator: since adapter 0.53.0 a responder whose slot was moved
|
|
64
|
-
to another session leaves its accepted work `accepted` and unanswered until the slot is taken back
|
|
65
|
-
hands it over
|
|
64
|
+
to another session leaves its accepted work `accepted` and unanswered until the slot is taken back, or, once Fabric offers the operator's move,
|
|
65
|
+
moved back to it (a responder from adapter 0.60.0 finds that; an older one comes back only by take-back), or until Fabric hands it over (CT-175), which is not the operator's hold below; tell
|
|
66
66
|
the person, and cancel it if it is past what they can wait. Never resend it under
|
|
67
|
-
a new key while it is open; follow "When nobody answers" below. A request without a deadline gets one seven
|
|
68
|
-
(DEC-0098) and then reads `expired`: if the ask still stands, submit it again with a new key and record the wait
|
|
69
|
-
new request.
|
|
67
|
+
a new key while it is open; follow "When nobody answers" below. A queued or claimed request without a deadline gets one seven
|
|
68
|
+
days out (DEC-0098) and then reads `expired`: if the ask still stands, submit it again with a new key and record the wait
|
|
69
|
+
on the new request. An accepted one is kept past its deadline (C4) and never expires: follow "When nobody answers".
|
|
70
70
|
`settledBy: "owner"` on a request (DEC-0081) means the estate owner settled it after its responder was gone: read it as
|
|
71
71
|
the owner's attestation, not the responder's.
|
|
72
72
|
|
|
@@ -100,6 +100,20 @@ A request to a Project whose owner is not working now would wait for nobody. Aft
|
|
|
100
100
|
the files you would touch (`git ls-remote origin 'refs/agent-sync/leases/*'` in its repository). `noResponder` in
|
|
101
101
|
the receipt says nobody serves the capability now. A capability or Project that is `held` is the operator's stop,
|
|
102
102
|
not "nobody live": wait, and make no change there.
|
|
103
|
+
An `active` responder does not prove your request is being worked. A slot moved to another session (the operator's move,
|
|
104
|
+
contract DEC-0140 PR-2, once Fabric offers it, or another session's replacement) keeps the requests the old holder had accepted: they stay `accepted` with the old holder's
|
|
105
|
+
attempt, which nobody works until the new holder takes them over by a transfer or the old service gets the slot back
|
|
106
|
+
(its take-back, or a move back to a responder from adapter 0.60.0), while Fabric's thread view shows the
|
|
107
|
+
new holder as "Answers it" and its "requests no agent answers now" leaves them out (FAA-172). The board shows a
|
|
108
|
+
requester no attempt or generation (`com.get` gives the request's `state`, `effect` and `revision`, never its attempt), so you cannot tell
|
|
109
|
+
this from slow work: when your request has stayed `accepted` with no progress note or reply past what the work
|
|
110
|
+
should take, ask on its thread (`com.reply`) whether it is being worked. If nobody answers, cancel it first: while
|
|
111
|
+
its `effect` is still `not_started`, `com.cancel` moves it to `cancelled` (nothing has happened yet). Only when the
|
|
112
|
+
cancel's receipt shows the request `cancelled`, submit it again and read the new request from this step (an active
|
|
113
|
+
responder is step 2; step 3 only when nobody is live and it stays queued and unclaimed); an
|
|
114
|
+
accepted request never expires, and the old service would carry it out by a transfer once it gets the slot back, so
|
|
115
|
+
it is never adapted while still accepted. `outcome_unknown` means the work began meanwhile: tell the person and
|
|
116
|
+
change nothing.
|
|
103
117
|
2. **Live:** wait under a watcher (below) and work on independent tasks meanwhile; never touch files a live session
|
|
104
118
|
holds.
|
|
105
119
|
3. **Nobody live, and the change is small.** Small means all three:
|
|
@@ -114,8 +128,8 @@ A request to a Project whose owner is not working now would wait for nobody. Aft
|
|
|
114
128
|
allow. Then reply on the request's thread, one line of body text (not a `reaction`, which only an announcement's
|
|
115
129
|
reply carries): **adapted by** your Project, the merge commit, and what the owner should look at. The owner's next
|
|
116
130
|
session reads it and may revert it. One such change per target repository at a time. Your own reply does not answer
|
|
117
|
-
your own request:
|
|
118
|
-
the owner to see until its deadline; do not cancel it.
|
|
131
|
+
your own request: a queued one stays on the board, on its thread and on Fabric's home ("requests no agent answers
|
|
132
|
+
now"), for the owner to see until its deadline; do not cancel it.
|
|
119
133
|
4. **Not small** (a decision, a contract, a release, a large change): the request stays queued for its owner until its
|
|
120
134
|
deadline (seven days by default); never take a decision that is the owner's, record the wait in one line (what it
|
|
121
135
|
waits on, which watcher acts) and go on with your own work. An expiry posts no message, so the mail watcher does
|