@passioncode-ai/fabric-agent-adapter 0.18.0 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,66 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.19.0 - 2026-10-12
4
+
5
+ Settings backups, every spending limit and the feed client's token header in both kits. These are the
6
+ service-side surfaces the pinned contract already carries: DEC-0025 and DEC-0027 (FAA-13).
7
+
8
+ ### Added
9
+
10
+ - **Settings backups (`fabric-settings-backup/1`, FAC-SEM-035).** Each function exists in both kits.
11
+ - **Location.** Snapshots live under `<root>/settings-backups/<id>/<instance>/`, outside the data directory.
12
+ `<root>` is per system: `~/Library/Application Support/PassionCode`, `%APPDATA%\PassionCode` or
13
+ `$XDG_CONFIG_HOME/PassionCode`.
14
+ - **Writing.** `make_settings_backup` builds a snapshot from allow-listed tables of JSON scalars. It refuses
15
+ secret tables, credential columns and fractions.
16
+ - `write_settings_backup` writes atomically, 0600 in a 0700 folder.
17
+ - It does not rewrite an unchanged snapshot.
18
+ - It removes a temporary file an interrupted write left behind.
19
+ - It keeps the 14 newest snapshots.
20
+ - `backup_due` decides when the next snapshot is due.
21
+ - **Reading.** `read_settings_backup` refuses a damaged file and names why.
22
+ - `newest_intact_backup` skips damaged files and empty snapshots.
23
+ - `restore_plan` adds rows and never overwrites them. It names what it drops, leaves to defaults or refuses.
24
+ - **The checksum** uses the settings canonical form: `settings_canonical_json` and `settings_backup_digest`.
25
+ They are checked against the contract's vectors.
26
+ - **Parity.** Both kits write byte-identical files.
27
+ - **Every spending limit (DEC-0027, FAC-SEM-031).**
28
+ - `usage_report` and `usageReport` take a `budgets` list.
29
+ - A machine day or month limit is counted from the receipts, so it always agrees with `budget`.
30
+ - A per-order limit gets `spentUsd: null`.
31
+ - The report is refused when its limits break FAC-SEM-031.
32
+ - `usage_limit_problems` and `usageLimitProblems` check a report.
33
+ - **The feed client (FAC-SEM-036).** `token_header` and `tokenHeader` give the one header the descriptor
34
+ declares. `check_service.py` now sends its token through it.
35
+ - **An independent review found 20 moderate defects; all are fixed with tests.**
36
+ - **The readers follow the schema exactly**, and the two kits accept the same files:
37
+ - a trailing newline is refused (Python matches a whole string now);
38
+ - a timestamp is any RFC 3339 date-time with a real calendar date, including an offset;
39
+ - a string holds at most 1 MiB and a row at most 512 values;
40
+ - every `extensions` key is a URI;
41
+ - NaN is refused;
42
+ - in Node, invalid UTF-8 and a `1.0` integer are refused the way Python refuses them.
43
+ - **A damaged file never stops anything.** The scan skips it, the digest does not crash, a damaged newest file
44
+ does not suppress a good snapshot, and an impossible file name makes a snapshot due.
45
+ - **Safer writes:**
46
+ - only a temporary file older than an hour is removed;
47
+ - the snapshot just written is never pruned, even after the clock steps back.
48
+ - **The restore preview** refuses a key the snapshot holds twice.
49
+ - **Limits:**
50
+ - a machine limit over a rolling window is counted from the receipts;
51
+ - a limit that is not an object is refused.
52
+ - **Both descriptor validators** check `auth.header` and `auth.scheme` the way the schema does.
53
+ - **Tests.**
54
+ - The contract's fixtures at the pin are copied to `test/fixtures/contract/dec-0025-0027/`.
55
+ - The Windows CI job runs both kits' new cases.
56
+ - Mutation testing: 79 planted defects, all caught.
57
+ - **Docs.**
58
+ - `references/settings-backups.md` is new.
59
+ - `usage.md` covers every limit.
60
+ - `events-and-notifications.md` covers reading a feed.
61
+ - Updated: `SKILL.md` step 5 and the lifecycle reference's directory table.
62
+ - The scenario eval `settings-backups-and-every-limit` and two triggers came first.
63
+
3
64
  ## 0.18.0 - 2026-10-12
4
65
 
5
66
  Services on Linux and Windows in both kits (contract DEC-0032, DEC-0033; FAA-21): the supervisor files the
package/README.md CHANGED
@@ -230,8 +230,8 @@ FABRIC_CONTRACT_OLD=/path/to/legacy-contract FABRIC_CONTRACT_NEW=/path/to/curren
230
230
  ```
231
231
 
232
232
  Without both environment variables, those tests explicitly skip as `NOT_RUN`.
233
- This source checkout is 0.18.0. 0.17.0 is the latest published GitHub/npm release (2026-10-12; 0.7.0 and 0.11.0
234
- were never released); 0.18.0 is published by its `v0.18.0` tag. Source delivery does not update installed skills or live sessions.
233
+ This source checkout is 0.19.0. 0.18.0 is the latest published GitHub/npm release (2026-10-12; 0.7.0 and 0.11.0
234
+ were never released); 0.19.0 is published by its `v0.19.0` tag. Source delivery does not update installed skills or live sessions.
235
235
 
236
236
  ## License
237
237
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@passioncode-ai/fabric-agent-adapter",
3
- "version": "0.18.0",
3
+ "version": "0.19.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.",
6
- "version": "0.18.0",
6
+ "version": "0.19.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.18.0"
8
+ version: "0.19.0"
9
9
  contract-version: "0.1.0"
10
10
  contract-commit: "0cacd08b0e95b6abf131eae1206b60792138bc3c"
11
11
  ---
@@ -16,7 +16,7 @@ license: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
16
16
  compatibility: Python 3.9+ or Node.js 20+ for the kits; 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.
17
17
  metadata:
18
18
  author: PassionCode.ai
19
- version: "0.18.0"
19
+ version: "0.19.0"
20
20
  contract-version: "0.1.0"
21
21
  extension: "fabric-service/0.1"
22
22
  extension-commit: "0cacd08b0e95b6abf131eae1206b60792138bc3c"
@@ -228,8 +228,12 @@ log, retention and notification policy.
228
228
 
229
229
  An agent that calls paid models reports **what it spent**: one receipt per call from the
230
230
  provider's own usage numbers, served behind the token at `surfaces.usage` as the DEC-0021 usage
231
- report. An unknown cost is null, never 0. Read [the usage reference](references/usage.md)
232
- before wiring it.
231
+ report. An unknown cost is null, never 0. It lists **every** spending limit the service applies
232
+ in `budgets`, enforced or not. Read [the usage reference](references/usage.md) before wiring it.
233
+
234
+ A service whose store holds what the operator decided keeps **settings backups** outside its data
235
+ directory, restores only on a fresh database and never overwrites. Read
236
+ [the settings backups reference](references/settings-backups.md).
233
237
 
234
238
  ## Step 6 — verify
235
239
 
@@ -11,6 +11,15 @@
11
11
 
12
12
  The feed is a view: never copy rows into a second store that can drift from the first.
13
13
 
14
+ ## Reading a feed (FAC-SEM-036)
15
+
16
+ A client of `GET /fabric/v1/events` — or of any token-protected route — sends the token in the
17
+ header the descriptor's `auth.header` names, in the form `auth.scheme` names: the raw token for
18
+ `none`, `Bearer <token>` for `Bearer`. Never assume `Authorization: Bearer`: a client that did
19
+ was refused on every poll and silently dropped every notify event. `token_header(auth, token)` /
20
+ `tokenHeader(auth, token)` gives the one header; the token never travels in another header or
21
+ in the URL.
22
+
14
23
  ## Writing an event
15
24
 
16
25
  - `kind`: dotted, stable, past tense or state (`job.failed`, `job.awaiting_choice`,
@@ -88,6 +88,7 @@ remove; the back-off only stops the loop from costing the machine.
88
88
  | logs | `~/Library/Logs/<id>/` | `${XDG_STATE_HOME:-~/.local/state}/<id>/logs/` | `%LOCALAPPDATA%\<id>\Logs\` |
89
89
  | cache (deletable any time) | `~/Library/Caches/<id>/` | `${XDG_CACHE_HOME:-~/.cache}/<id>/` | `%LOCALAPPDATA%\<id>\Cache\` |
90
90
  | code | `~/.local/share/<id>/releases/…` | same | under `%LOCALAPPDATA%` too, never a network share |
91
+ | settings backups ([DEC-0025](settings-backups.md)) | `~/Library/Application Support/PassionCode/settings-backups/<id>/<instance>/` | `${XDG_CONFIG_HOME:-~/.config}/PassionCode/settings-backups/…` | `%APPDATA%\PassionCode\settings-backups\…` |
91
92
 
92
93
  A repository that versions the data itself (a registry, a plan) may hold the data; the
93
94
  service's own code checkout and its releases may not. Directories 0700, secret files 0600. A store carries a schema version and the service
@@ -0,0 +1,67 @@
1
+ # Settings backups — what the operator decided survives the data directory (DEC-0025)
2
+
3
+ Fabric Agent Contract DEC-0025, `docs/specification/service.md` → *Settings backup*, schema
4
+ `schemas/settings-backup.schema.json`, rule `FAC-SEM-035`. A service whose store holds what the operator
5
+ decided — which projects it serves, how each is bound, which secret slot each binding uses, their overrides —
6
+ SHOULD keep settings backups, so a lost, purged or wrongly migrated data directory costs the operator nothing.
7
+
8
+ ## Where
9
+
10
+ `<root>/settings-backups/<id>/<instance>/settings-<UTC %Y%m%dT%H%M%S%fZ>.json`, outside the data directory:
11
+
12
+ | System | `<root>` (`settings_backup_root` / `settingsBackupRoot`) |
13
+ |---|---|
14
+ | macOS | `~/Library/Application Support/PassionCode` |
15
+ | Windows | `%APPDATA%\PassionCode` |
16
+ | Linux | `$XDG_CONFIG_HOME/PassionCode`, default `~/.config/PassionCode` |
17
+
18
+ `settings_backup_dir(id, instance)` / `settingsBackupDir(id, instance)` names the folder. Directories are 0700
19
+ (on Windows a protected ACL), files 0600, writes atomic.
20
+
21
+ ## Take a snapshot
22
+
23
+ ```python
24
+ dirs = fs.settings_backup_dir(ID, INSTANCE)
25
+ if fs.backup_due(dirs): # none yet, or the newest is older than 24 hours
26
+ doc = fs.make_settings_backup(service_id=ID, instance=INSTANCE, version=VERSION, schema=SCHEMA_VERSION,
27
+ tables=allow_listed_tables(db), reason="start") # or daily, uninstall, manual
28
+ fs.write_settings_backup(doc, dirs) # None when unchanged; keeps the 14 newest
29
+ ```
30
+
31
+ ```js
32
+ const dir = k.settingsBackupDir(ID, INSTANCE);
33
+ if (k.backupDue(dir)) k.writeSettingsBackup(k.makeSettingsBackup({ id: ID, instance: INSTANCE, version, schema, tables, reason: 'daily' }), dir);
34
+ ```
35
+
36
+ - **Tables on an allow-list**, each `{"columns": [...], "rows": [[...], ...]}` of JSON scalars. A fraction is its
37
+ decimal string (`"0.25"`), binary data is never stored, and an integer stays within ±(2^53 − 1).
38
+ - **Secrets by name only.** `make_settings_backup` refuses a table that can only hold connections, tokens,
39
+ secrets, credentials, passwords, login codes or sessions, and a column whose name says it holds a credential
40
+ value (`access_token`, `client_secret`); `secret_name` and `token_slot` pass. The names cannot see values, so
41
+ back up what is on the allow-list, never everything except a deny-list.
42
+ - **When:** at start when `backup_due`, then daily; on uninstall a last one, read-only (open the store without
43
+ migrating or writing). An unchanged snapshot is not rewritten; a temporary file an interrupted write left is
44
+ removed first. A fresh, empty database writes none: it would hide the good snapshot before it is restored.
45
+ - Both kits write byte-identical files (`test/fabric-service-backups.test.mjs`, last case).
46
+
47
+ ## Read and restore
48
+
49
+ `read_settings_backup` / `readSettingsBackup` refuses a file whose checksum, counts or row widths disagree with
50
+ its tables, that carries a value with no canonical form, or whose `format` it does not know, naming why
51
+ (`FAC-SEM-035`). The checksum is SHA-256 of the settings canonical form of `tables`
52
+ (`settings_canonical_json` / `settingsCanonicalJson`: Python's `json.dumps(sort_keys=True,
53
+ ensure_ascii=False)`).
54
+
55
+ Restore **adds and never overwrites**:
56
+
57
+ ```python
58
+ path, doc, skipped = fs.newest_intact_backup(dirs) # the newest intact snapshot with content; damaged ones named
59
+ plan = fs.restore_plan(doc, {"projects": {"columns": [...], "key": ["id"], "required": ["id", "name"],
60
+ "existing": existing_keys("projects")}})
61
+ # plan["tables"]["projects"]: insert, present, droppedColumns, defaultedColumns, missingRequired, refused
62
+ ```
63
+
64
+ - **Automatic restore** only on a *fresh* database — no domain records **and** the service has never started —
65
+ so a restart never undoes a deletion. Insert `plan[...]["insert"]` and add every row the database refuses to
66
+ `refused` with its key and reason.
67
+ - **Manual restore** shows the plan first; applying it is the operator's act, never an agent's.
@@ -50,6 +50,29 @@ left out.
50
50
  **Budget.** Pass `budget` only if the service **enforces** that limit itself. The report states
51
51
  it, and the host only shows it.
52
52
 
53
+ **Every limit (DEC-0027).** Pass `budgets` to list every spending limit the service applies,
54
+ enforced or not, so the spend view shows all of them — the operator asked for that on 2026-10-07:
55
+
56
+ ```python
57
+ ledger.report(service_id=ID, budget={"period": "month", "limitUsd": 100}, budgets=[
58
+ {"id": "machine.monthly", "scope": "machine", "kind": "monthly", "period": "month", "limitUsd": 100, "enforced": True},
59
+ {"id": "project.demo.daily", "scope": "project", "subject": "demo", "kind": "daily", "windowSeconds": 86400,
60
+ "limitUsd": 5, "spentUsd": 0.31, "enforced": True},
61
+ {"id": "job.cap", "scope": "job", "kind": "per_job", "limitUsd": 2, "enforced": True},
62
+ ])
63
+ ```
64
+
65
+ - A machine limit with a `period` and no `spentUsd` is counted by the kit from the receipts, the
66
+ way `budget` is, so `budget` and its twin in `budgets` never disagree. A per-order limit
67
+ (`per_job`, `approval`) gets `spentUsd: null`. Every other limit states its own spend.
68
+ - `scope` is `machine`, `project`, `pool` or `job`; a `project` or `pool` limit names its
69
+ `subject`. A limit has at most one window — `period`, `windowSeconds` or `since` — and it agrees
70
+ with the kind (`daily`: the day or 86400 s; `monthly`: the month or 28–31 days; `velocity`: a
71
+ rolling window). `tripped: true` (work stopped by it now) needs `enforced: true`. An unknown
72
+ kind is shown generically, never refused.
73
+ - The kits refuse a report that breaks these (`usage_limit_problems` / `usageLimitProblems`,
74
+ `FAC-SEM-031`). A breach is not refused: the report shows it, and the host shows it first.
75
+
53
76
  Call `ledger.prune()` at start (or daily) to keep the file at the reported window (LC-12).
54
77
 
55
78
  `JsonlUsageLedger` appends 0600 JSON lines. A line torn by a killed writer is skipped, and the
@@ -162,10 +162,9 @@ class Probe:
162
162
  conn.close()
163
163
 
164
164
  def auth_headers(self) -> Dict[str, str]:
165
- auth = self.d.get("auth", {})
166
- header = auth.get("header", "Authorization")
167
- value = self.token or ""
168
- return {header: ("Bearer " + value) if auth.get("scheme", "Bearer") == "Bearer" else value}
165
+ # The header and form the descriptor declares, never an assumed `Authorization: Bearer` (FAC-SEM-036).
166
+ name, value = fs.token_header(self.d.get("auth"), self.token or "")
167
+ return {name: value}
169
168
 
170
169
  # descriptor ------------------------------------------------------------
171
170
  def descriptor_rules(self) -> None: