@passioncode-ai/passioncode 0.1.28 → 0.1.30
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 +28 -0
- package/COMMERCIAL-LICENSE.md +1 -1
- package/README.md +1 -1
- package/family.json +2 -2
- package/package.json +1 -1
- package/payload/.claude-plugin/marketplace.json +3 -3
- package/payload/manifest.json +12 -12
- package/payload/plugins/fabric-agent-adapter/.claude-plugin/plugin.json +1 -1
- package/payload/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/SKILL.md +9 -3
- package/payload/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/adapt_project.py +39 -10
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +23 -10
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/lifecycle.md +77 -14
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/protocol.md +5 -3
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/surfaces-and-auth.md +6 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/usage.md +63 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service.py +106 -3
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-interop.mjs +42 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-service.mjs +317 -8
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_interop.py +53 -1
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_service.py +472 -15
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/sample_service.py +23 -10
- package/payload/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +7 -3
- package/payload/plugins/observatory-log/.claude-plugin/plugin.json +1 -1
- package/payload/plugins/observatory-log/hooks/session-start.sh +91 -4
- package/payload/plugins/observatory-log/skills/explaining-changes/SKILL.md +1 -1
- package/payload/plugins/observatory-log/skills/handling-secrets/SKILL.md +12 -2
- package/payload/plugins/observatory-log/skills/tracking-resources/SKILL.md +1 -1
- package/payload/plugins/passioncode/.claude-plugin/plugin.json +1 -1
- package/payload/plugins/passioncode/skills/working-in-passioncode/SKILL.md +6 -4
- package/payload/plugins/passioncode/skills/working-in-passioncode/references/publishing.md +4 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,33 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.30 - 2026-10-07
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
|
|
7
|
+
- **Fabric Agent Adapter is pinned at `v0.8.0`** (it was `v0.6.3`). Its service kit meets the
|
|
8
|
+
organization's lifecycle contract (0.7.0: a launchd install respects an operator's disable, an
|
|
9
|
+
`ExitTimeOut` default), agents can report what they spend (`make_usage_receipt`, `usage_report`,
|
|
10
|
+
Fabric Agent Contract DEC-0021), and a service may keep its MCP credential apart from the host's
|
|
11
|
+
token (DEC-0024). Bundles issued under the earlier contract revisions stay valid.
|
|
12
|
+
- **Observatory Log is pinned at `v0.18.0`** (plugin 0.18.0; it was `v0.17.3`), the Project
|
|
13
|
+
Observatory 0.18.0 skills. That release installs only updates whose `SHA256SUMS` is signed by
|
|
14
|
+
the organization's release key, so the hook's daily update of an older engine now refuses an
|
|
15
|
+
unsigned or foreign-signed release instead of installing it.
|
|
16
|
+
|
|
17
|
+
## 0.1.29 - 2026-10-05
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- **Observatory Log is pinned at `v0.17.3`** (plugin 0.17.3; it was `v0.16.0`). Its
|
|
22
|
+
SessionStart hook keeps Project Observatory current:
|
|
23
|
+
- for an engine at 0.17.0 or later, it schedules the engine's hourly maintenance job where
|
|
24
|
+
it is missing;
|
|
25
|
+
- for an engine from 0.7.0 to 0.16.0, which has no updater of its own, it installs the newer
|
|
26
|
+
release once a day with that engine's `full update --apply`, unless the person turned
|
|
27
|
+
automatic updates off.
|
|
28
|
+
|
|
29
|
+
Both run detached, at most every six hours or once a day, and never hold up a session.
|
|
30
|
+
|
|
3
31
|
## 0.1.28 - 2026-10-05
|
|
4
32
|
|
|
5
33
|
The launcher meets the organization's
|
package/COMMERCIAL-LICENSE.md
CHANGED
|
@@ -5,7 +5,7 @@ This repository is open source under the [GNU Affero General Public License v3.0
|
|
|
5
5
|
meet the AGPL's terms — for example inside a closed-source product, or as a modified hosted
|
|
6
6
|
service whose source is not published.
|
|
7
7
|
|
|
8
|
-
To obtain a commercial license, write to **
|
|
8
|
+
To obtain a commercial license, tell us about your use at **https://passioncode.ai/business/**, or write to **commercial@passioncode.ai**.
|
|
9
9
|
|
|
10
10
|
SPDX: `AGPL-3.0-only OR LicenseRef-PassionCode-Commercial`. Copyright (c) 2026 Siarhei Sheleh.
|
|
11
11
|
Contributions are accepted under [CLA.md](CLA.md), which allows this dual licence.
|
package/README.md
CHANGED
|
@@ -115,7 +115,7 @@ reads a member from `<dir>/<repository name>`, and `PASSIONCODE_CHECKOUT_<MEMBER
|
|
|
115
115
|
## License
|
|
116
116
|
|
|
117
117
|
Open source under the [GNU AGPL-3.0](LICENSE). A [commercial license](COMMERCIAL-LICENSE.md) is
|
|
118
|
-
available for use that does not meet the AGPL's terms —
|
|
118
|
+
available for use that does not meet the AGPL's terms — [passioncode.ai/business](https://passioncode.ai/business/).
|
|
119
119
|
Versions before 0.1.12 were released under PolyForm Noncommercial or Internal Use (v0.1.5 to
|
|
120
120
|
v0.1.11) and MIT (v0.1.4 and earlier); each keeps the licence it was released under.
|
|
121
121
|
Contributions are accepted under [CLA.md](CLA.md) ([CONTRIBUTING.md](CONTRIBUTING.md)).
|
package/family.json
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"name": "fabric-agent-adapter",
|
|
9
9
|
"displayName": "Fabric Agent Adapter",
|
|
10
10
|
"repo": "passioncode-ai/fabric-agent-adapter",
|
|
11
|
-
"ref": "v0.
|
|
11
|
+
"ref": "v0.8.0",
|
|
12
12
|
"kind": "plugin",
|
|
13
13
|
"path": "plugins/fabric-agent-adapter",
|
|
14
14
|
"legacyPluginIds": [
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"name": "observatory-log",
|
|
23
23
|
"displayName": "Observatory Log",
|
|
24
24
|
"repo": "passioncode-ai/project-observatory-dashboard",
|
|
25
|
-
"ref": "v0.
|
|
25
|
+
"ref": "v0.18.0",
|
|
26
26
|
"kind": "plugin",
|
|
27
27
|
"path": "observatory/engine/skill/plugins/observatory-log",
|
|
28
28
|
"legacyPluginIds": [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@passioncode-ai/passioncode",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.30",
|
|
4
4
|
"description": "The PassionCode.ai agent skill set \u2014 Fabric Agent Adapter, Observatory Log and the organisation's working rules \u2014 for Claude Code and every other agent on the machine, updated as one set.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"passioncode": "bin/passioncode.js"
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"name": "fabric-agent-adapter",
|
|
12
12
|
"displayName": "Fabric Agent Adapter",
|
|
13
13
|
"source": "./plugins/fabric-agent-adapter",
|
|
14
|
-
"version": "0.
|
|
14
|
+
"version": "0.8.0",
|
|
15
15
|
"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.",
|
|
16
16
|
"author": {
|
|
17
17
|
"name": "PassionCode.ai",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"name": "observatory-log",
|
|
24
24
|
"displayName": "Observatory Log",
|
|
25
25
|
"source": "./plugins/observatory-log",
|
|
26
|
-
"version": "0.
|
|
26
|
+
"version": "0.18.0",
|
|
27
27
|
"description": "Records what changed in a watched project at the end of every turn, and asks for the one thing a diff cannot contain: why.",
|
|
28
28
|
"author": {
|
|
29
29
|
"name": "PassionCode.ai",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"name": "passioncode",
|
|
36
36
|
"displayName": "PassionCode.ai",
|
|
37
37
|
"source": "./plugins/passioncode",
|
|
38
|
-
"version": "0.1.
|
|
38
|
+
"version": "0.1.30",
|
|
39
39
|
"description": "The organisation's working rules for every contributor's agent (working-in-passioncode), and a once-a-day check that keeps the PassionCode.ai set current.",
|
|
40
40
|
"author": {
|
|
41
41
|
"name": "PassionCode.ai",
|
package/payload/manifest.json
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"family": "passioncode",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.30",
|
|
4
4
|
"release": true,
|
|
5
5
|
"members": [
|
|
6
6
|
{
|
|
7
7
|
"name": "fabric-agent-adapter",
|
|
8
8
|
"displayName": "Fabric Agent Adapter",
|
|
9
9
|
"repo": "passioncode-ai/fabric-agent-adapter",
|
|
10
|
-
"ref": "v0.
|
|
11
|
-
"commit": "
|
|
12
|
-
"version": "0.
|
|
10
|
+
"ref": "v0.8.0",
|
|
11
|
+
"commit": "726646da8b8d2ec51ae5f911e1b3058d563d5cdc",
|
|
12
|
+
"version": "0.8.0",
|
|
13
13
|
"via": "clone of https://***@github.com/passioncode-ai/fabric-agent-adapter.git",
|
|
14
14
|
"skills": [
|
|
15
15
|
"adapting-projects-to-fabric",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"legacyMarketplaces": [
|
|
23
23
|
"fabric-agent-adapter"
|
|
24
24
|
],
|
|
25
|
-
"contentHash": "sha256:
|
|
25
|
+
"contentHash": "sha256:8bf9584283471da6dcb4c2dbcb6be26a9ff3a042634a84976d5bb7ff479feba7",
|
|
26
26
|
"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.",
|
|
27
27
|
"author": {
|
|
28
28
|
"name": "PassionCode.ai",
|
|
@@ -34,9 +34,9 @@
|
|
|
34
34
|
"name": "observatory-log",
|
|
35
35
|
"displayName": "Observatory Log",
|
|
36
36
|
"repo": "passioncode-ai/project-observatory-dashboard",
|
|
37
|
-
"ref": "v0.
|
|
38
|
-
"commit": "
|
|
39
|
-
"version": "0.
|
|
37
|
+
"ref": "v0.18.0",
|
|
38
|
+
"commit": "4c68decefd213ccbb808a6f0ed5dfe72c1760d58",
|
|
39
|
+
"version": "0.18.0",
|
|
40
40
|
"via": "clone of https://***@github.com/passioncode-ai/project-observatory-dashboard.git",
|
|
41
41
|
"skills": [
|
|
42
42
|
"explaining-changes",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"legacyMarketplaces": [
|
|
50
50
|
"observatory-log"
|
|
51
51
|
],
|
|
52
|
-
"contentHash": "sha256:
|
|
52
|
+
"contentHash": "sha256:3e93bbc01672e5595f5ba95412b4cc7eed633f559a121ec3f9908efe12cf4f59",
|
|
53
53
|
"description": "Records what changed in a watched project at the end of every turn, and asks for the one thing a diff cannot contain: why.",
|
|
54
54
|
"author": {
|
|
55
55
|
"name": "PassionCode.ai",
|
|
@@ -61,16 +61,16 @@
|
|
|
61
61
|
"name": "passioncode",
|
|
62
62
|
"displayName": "PassionCode.ai",
|
|
63
63
|
"repo": "passioncode-ai/passioncode",
|
|
64
|
-
"ref": "v0.1.
|
|
64
|
+
"ref": "v0.1.30",
|
|
65
65
|
"commit": null,
|
|
66
|
-
"version": "0.1.
|
|
66
|
+
"version": "0.1.30",
|
|
67
67
|
"via": "this repository",
|
|
68
68
|
"skills": [
|
|
69
69
|
"working-in-passioncode"
|
|
70
70
|
],
|
|
71
71
|
"legacyPluginIds": [],
|
|
72
72
|
"legacyMarketplaces": [],
|
|
73
|
-
"contentHash": "sha256:
|
|
73
|
+
"contentHash": "sha256:f9ed14b545885bea0bdb2967253eeb53850aeb1e8880a2e9aaf8c840faae4b05",
|
|
74
74
|
"description": "The organisation's working rules for every contributor's agent (working-in-passioncode), and a once-a-day check that keeps the PassionCode.ai set current.",
|
|
75
75
|
"author": {
|
|
76
76
|
"name": "PassionCode.ai",
|
|
@@ -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.
|
|
6
|
+
"version": "0.8.0",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "PassionCode.ai",
|
|
9
9
|
"url": "https://passioncode.ai/"
|
|
@@ -5,9 +5,9 @@ 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.8.0"
|
|
9
9
|
contract-version: "0.1.0"
|
|
10
|
-
contract-commit: "
|
|
10
|
+
contract-commit: "23f9fda4c05f8a3852246ee98d4f2adf74ed0875"
|
|
11
11
|
---
|
|
12
12
|
|
|
13
13
|
# Adapting projects to Fabric
|
|
@@ -70,7 +70,13 @@ Use exactly:
|
|
|
70
70
|
|
|
71
71
|
- contract version `0.1.0`;
|
|
72
72
|
- repository `https://github.com/passioncode-ai/fabric-agent-contract`;
|
|
73
|
-
- commit `
|
|
73
|
+
- commit `23f9fda4c05f8a3852246ee98d4f2adf74ed0875`.
|
|
74
|
+
|
|
75
|
+
This is the default for new bundles. When checking an issued legacy bundle, preserve its
|
|
76
|
+
explicit supported lock and use that exact clean checkout. Never silently upgrade or
|
|
77
|
+
downgrade it. Unknown repository/version/SHA locks and mismatched checkouts fail; missing
|
|
78
|
+
dependencies remain `NOT_RUN`. Capability names may contain underscores at this default
|
|
79
|
+
revision; declaring a COM name does not confer a grant, admission or wider authority.
|
|
74
80
|
|
|
75
81
|
Read the pinned contract's guide
|
|
76
82
|
`docs/guides/connecting-compatible-agents.md`, the selected profile specification, and
|
|
@@ -16,7 +16,11 @@ from urllib.parse import urlparse
|
|
|
16
16
|
|
|
17
17
|
CONTRACT_VERSION = "0.1.0"
|
|
18
18
|
CONTRACT_REPOSITORY = "https://github.com/passioncode-ai/fabric-agent-contract"
|
|
19
|
-
CONTRACT_COMMIT = "
|
|
19
|
+
CONTRACT_COMMIT = "23f9fda4c05f8a3852246ee98d4f2adf74ed0875"
|
|
20
|
+
# #region supported-contract-revisions — docs: README.md#contract-pin
|
|
21
|
+
# Only this declaration may name a legacy revision in live files.
|
|
22
|
+
SUPPORTED_CONTRACT_COMMITS = (CONTRACT_COMMIT, "9091d3d6606b0c4b5591a38c3674356a01fd7f55", "df55c8c54a23251342a7ee57ba95642b7eb39e61", "2ce392291c6668598d12cd38327e24696b5ca15c")
|
|
23
|
+
# #endregion supported-contract-revisions
|
|
20
24
|
INTEROP_KEY = "https://fabric.passioncode.ai/agent-contract/extensions/interop/0.1"
|
|
21
25
|
MCP_REVISION = "2026-07-28"
|
|
22
26
|
A2A_VERSION = "1.0"
|
|
@@ -357,7 +361,9 @@ def _walk_keys(value: Any, prefix: str = "") -> Iterable[Tuple[str, Any]]:
|
|
|
357
361
|
yield from _walk_keys(item, "%s[%d]" % (prefix, index))
|
|
358
362
|
|
|
359
363
|
|
|
360
|
-
def _contract_shape(manifest: Path, contract: Path) -> Tuple[str, List[str]]:
|
|
364
|
+
def _contract_shape(manifest: Path, contract: Path, selected_commit: str) -> Tuple[str, List[str]]:
|
|
365
|
+
if selected_commit not in SUPPORTED_CONTRACT_COMMITS:
|
|
366
|
+
return "FAIL", ["unsupported selected contract revision"]
|
|
361
367
|
if not contract.is_dir():
|
|
362
368
|
return "NOT_RUN", ["contract checkout is not a directory: %s" % contract]
|
|
363
369
|
try:
|
|
@@ -367,8 +373,19 @@ def _contract_shape(manifest: Path, contract: Path) -> Tuple[str, List[str]]:
|
|
|
367
373
|
).stdout.strip()
|
|
368
374
|
except (OSError, subprocess.CalledProcessError) as exc:
|
|
369
375
|
return "NOT_RUN", ["cannot read contract commit: %s" % exc]
|
|
370
|
-
if head !=
|
|
371
|
-
return "
|
|
376
|
+
if head != selected_commit:
|
|
377
|
+
return "FAIL", ["contract checkout is %s; selected lock requires %s" % (head, selected_commit)]
|
|
378
|
+
try:
|
|
379
|
+
dirty = subprocess.run(
|
|
380
|
+
["git", "status", "--porcelain", "--untracked-files=normal"], cwd=str(contract), check=True,
|
|
381
|
+
stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True,
|
|
382
|
+
).stdout.strip()
|
|
383
|
+
except (OSError, subprocess.CalledProcessError) as exc:
|
|
384
|
+
return "NOT_RUN", ["cannot verify clean contract checkout: %s" % exc]
|
|
385
|
+
if dirty:
|
|
386
|
+
return "FAIL", ["contract checkout has modifications; exact selected schema is required"]
|
|
387
|
+
if not (contract / "node_modules/.bin/tsx").is_file():
|
|
388
|
+
return "NOT_RUN", ["pinned contract dependencies are missing; run pnpm install --frozen-lockfile"]
|
|
372
389
|
|
|
373
390
|
code = """import {readFile} from 'node:fs/promises';
|
|
374
391
|
import {createValidator,validateDocument} from './src/validator.ts';
|
|
@@ -391,8 +408,10 @@ import {SCHEMA_PREFIX} from './src/contract.ts';
|
|
|
391
408
|
return "NOT_RUN", ["contract shape validation timed out"]
|
|
392
409
|
if run.returncode != 0:
|
|
393
410
|
detail = (run.stdout + "\n" + run.stderr).strip()
|
|
411
|
+
if run.returncode != 3:
|
|
412
|
+
return "NOT_RUN", [detail[-4000:] or "contract validator could not run"]
|
|
394
413
|
return "FAIL", [detail[-4000:] or "contract validator failed"]
|
|
395
|
-
return "PASS", ["manifest validated by
|
|
414
|
+
return "PASS", ["manifest validated by selected contract schema at %s" % selected_commit]
|
|
396
415
|
|
|
397
416
|
|
|
398
417
|
def check_project(root: Path, contract: Optional[Path]) -> Dict[str, Any]:
|
|
@@ -406,11 +425,19 @@ def check_project(root: Path, contract: Optional[Path]) -> Dict[str, Any]:
|
|
|
406
425
|
manifest = _load_json(manifest_path, errors)
|
|
407
426
|
lock = _load_json(root / "fabric-contract.lock.json", errors)
|
|
408
427
|
profile_kind = None
|
|
428
|
+
lock_errors: List[str] = []
|
|
409
429
|
if isinstance(lock, dict):
|
|
430
|
+
if lock.get("contract") != "fabric-agent-contract":
|
|
431
|
+
lock_errors.append("contract lock contract must be fabric-agent-contract")
|
|
432
|
+
if lock.get("repository") != CONTRACT_REPOSITORY:
|
|
433
|
+
lock_errors.append("contract lock repository must be %s" % CONTRACT_REPOSITORY)
|
|
410
434
|
if lock.get("version") != CONTRACT_VERSION:
|
|
411
|
-
|
|
412
|
-
if lock.get("commit")
|
|
413
|
-
|
|
435
|
+
lock_errors.append("contract lock version must be %s" % CONTRACT_VERSION)
|
|
436
|
+
if lock.get("commit") not in SUPPORTED_CONTRACT_COMMITS:
|
|
437
|
+
lock_errors.append("contract lock commit must be an explicitly supported immutable revision")
|
|
438
|
+
else:
|
|
439
|
+
lock_errors.append("contract lock must be an object")
|
|
440
|
+
errors.extend(lock_errors)
|
|
414
441
|
|
|
415
442
|
if isinstance(manifest, dict):
|
|
416
443
|
if manifest.get("contractVersion") != CONTRACT_VERSION:
|
|
@@ -462,8 +489,10 @@ def check_project(root: Path, contract: Optional[Path]) -> Dict[str, Any]:
|
|
|
462
489
|
|
|
463
490
|
shape_status = "NOT_RUN"
|
|
464
491
|
shape_receipts = ["pass --contract PATH pointing at the pinned checkout"]
|
|
465
|
-
if
|
|
466
|
-
shape_status, shape_receipts =
|
|
492
|
+
if lock_errors:
|
|
493
|
+
shape_status, shape_receipts = "FAIL", lock_errors
|
|
494
|
+
elif contract is not None and manifest_path.is_file():
|
|
495
|
+
shape_status, shape_receipts = _contract_shape(manifest_path, contract, lock["commit"])
|
|
467
496
|
|
|
468
497
|
local_status = "PASS" if not errors else "FAIL"
|
|
469
498
|
ready = local_status == "PASS" and shape_status == "PASS" and not warnings
|
|
@@ -5,9 +5,10 @@ description: >-
|
|
|
5
5
|
macOS machine, or making an ONLINE agent or dashboard (https origin, the remote placement) a
|
|
6
6
|
Fabric service — «сделай агенту дашборд», «локальный сервис агента», «онлайн-дашборд в Fabric»,
|
|
7
7
|
«подключи агента к Fabric Dashboards», "make this agent a local service", "make an online
|
|
8
|
-
dashboard a Fabric service", "fabric-service protocol", "add the well-known endpoint"
|
|
8
|
+
dashboard a Fabric service", "fabric-service protocol", "add the well-known endpoint", «расходы агента
|
|
9
|
+
на модели». Covers
|
|
9
10
|
fabric-service/0.1: surfaces, token and one-time login, state, one copy, launchd, descriptor,
|
|
10
|
-
well-known document, events; online: the https guard, the token-gated well-known document, the
|
|
11
|
+
well-known document, events, the usage report (spend per model); online: the https guard, the token-gated well-known document, the
|
|
11
12
|
__Host- cookie, registering it on the operator's computer; ships Python and Node kits, a TLS
|
|
12
13
|
sample and a live probe. NOT for a one-off script or cron job, a hosted product with no agent
|
|
13
14
|
behind it, the provider manifest (adapting-projects-to-fabric), or building Fabric Dashboards.
|
|
@@ -15,10 +16,10 @@ license: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
|
|
|
15
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 use lifecycle manager none until a systemd adapter exists). Dashboard handoff optionally uses Fabric Dashboards MCP link/host_status/open; without it, report unresolved host capability. The contract checkout is optional.
|
|
16
17
|
metadata:
|
|
17
18
|
author: PassionCode.ai
|
|
18
|
-
version: "0.
|
|
19
|
+
version: "0.8.0"
|
|
19
20
|
contract-version: "0.1.0"
|
|
20
21
|
extension: "fabric-service/0.1"
|
|
21
|
-
extension-commit: "
|
|
22
|
+
extension-commit: "23f9fda4c05f8a3852246ee98d4f2adf74ed0875"
|
|
22
23
|
---
|
|
23
24
|
|
|
24
25
|
# Building Fabric services
|
|
@@ -90,7 +91,9 @@ are in [`scripts/fabric_interop.py`](scripts/fabric_interop.py) and
|
|
|
90
91
|
2. **One copy, locked before any side effect.** Take the exclusive lock on
|
|
91
92
|
`<data>/service.lock` first — before resuming jobs, starting a scheduler, migrating
|
|
92
93
|
a store or even creating a token. Held → print one sentence naming the holder's pid
|
|
93
|
-
and exit **75**. Binding the port is not a lock.
|
|
94
|
+
and exit **75**. Binding the port is not a lock. Under launchd the kit backs off
|
|
95
|
+
in-process first (up to 300 s), so a duplicate is a few starts an hour, not a
|
|
96
|
+
`KeepAlive` respawn every 10 s. *(A second copy re-queued the first
|
|
94
97
|
copy's running jobs in its startup hook, before its bind failed.)*
|
|
95
98
|
3. **launchd is the only supervisor.** `RunAtLoad` true, `KeepAlive` true,
|
|
96
99
|
`ThrottleInterval` 10, `ExitTimeOut` above your drain time. Never start yourself
|
|
@@ -133,7 +136,7 @@ Startup order is the part that goes wrong; keep it exactly:
|
|
|
133
136
|
import fabric_service as fs
|
|
134
137
|
|
|
135
138
|
dirs = fs.service_dirs("example-agent")
|
|
136
|
-
lock = fs.hold_single_instance(dirs["data"]) # 1. lock — exits 75 if held
|
|
139
|
+
lock = fs.hold_single_instance(dirs["data"]) # 1. lock — exits 75 if held (backs off first under launchd)
|
|
137
140
|
token = fs.ensure_token(dirs["data"] / "service.token") # 2. only now touch state
|
|
138
141
|
log = fs.JsonlEventLog(dirs["data"] / "events.jsonl") # (or a view over your own log)
|
|
139
142
|
codes = fs.LoginCodes(dirs["data"] / "auth")
|
|
@@ -153,8 +156,10 @@ On every request call `check_request(port, host, origin, sec_fetch_site)` and an
|
|
|
153
156
|
| `POST /fabric/v1/login-code` | service token | `LoginCodes.issue()` |
|
|
154
157
|
| `GET /fabric/v1/login?code=` | the code | `LoginCodes.redeem(code)` → `Set-Cookie: session_cookie_header(...)`, 302 to the dashboard |
|
|
155
158
|
|
|
156
|
-
`SIGTERM` drains in-flight work
|
|
157
|
-
|
|
159
|
+
`SIGTERM` drains in-flight work, then exits — 10 s at most, inside `ExitTimeOut`
|
|
160
|
+
(`fs.Drain` / `new Drain()`: `with drain.work():` refuses new work once a stop began);
|
|
161
|
+
interrupted work resumes on the next start. Log through `RotatingLog` (5 × 5 MB, 0600)
|
|
162
|
+
and call `cap_stdout_log` once at start.
|
|
158
163
|
|
|
159
164
|
The MCP surface (`POST /mcp`, service token) is `fabric_interop.McpToolServer.handle`
|
|
160
165
|
in Python; jobs live in `fabric_interop.JobStore(dirs["data"] / "jobs")`, created after
|
|
@@ -175,8 +180,11 @@ The installer, not the service, owns the plist and the descriptor. Sequence:
|
|
|
175
180
|
2. `launchd_plist(...)` then `launchd_install(...)` — writes and lints the plist,
|
|
176
181
|
`bootout` and waits for the unload, `bootstrap` with retries on the transient I/O
|
|
177
182
|
error, then polls the well-known document until it answers with **this** identity.
|
|
178
|
-
|
|
179
|
-
|
|
183
|
+
A label the operator disabled stays disabled: only a first install enables.
|
|
184
|
+
3. Uninstall: `launchd_uninstall` (waits for the unload, removes the plist, resets the
|
|
185
|
+
override; `purge=True` removes data only when the operator asks), `remove_descriptor`.
|
|
186
|
+
4. After an upgrade, `prune_releases(releases_dir, current=…)` keeps the running release
|
|
187
|
+
and the one before it.
|
|
180
188
|
|
|
181
189
|
Code runs from an immutable release directory; an upgrade writes a new release,
|
|
182
190
|
rewrites the plist and restarts. Read [the lifecycle reference](references/lifecycle.md)
|
|
@@ -199,6 +207,11 @@ raises no banners of its own: the host is the one channel. Read
|
|
|
199
207
|
[the events reference](references/events-and-notifications.md) for mapping an existing
|
|
200
208
|
log, retention and notification policy.
|
|
201
209
|
|
|
210
|
+
An agent that calls paid models reports **what it spent**: one receipt per call from the
|
|
211
|
+
provider's own usage numbers, served behind the token at `surfaces.usage` as the DEC-0021 usage
|
|
212
|
+
report. An unknown cost is null, never 0. Read [the usage reference](references/usage.md)
|
|
213
|
+
before wiring it.
|
|
214
|
+
|
|
202
215
|
## Step 6 — verify
|
|
203
216
|
|
|
204
217
|
Run the probe against the live service — it reads the descriptor, then checks every
|
package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/lifecycle.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# Lifecycle — supervisor, install, release layout, state
|
|
2
2
|
|
|
3
|
+
This page implements the organization's
|
|
4
|
+
[product lifecycle contract](https://github.com/passioncode-ai/fabric-workspace/blob/main/knowledge/lifecycle.md)
|
|
5
|
+
(rules LC-01…LC-15) for `fabric-service/0.1` services. Where the two differ, the contract
|
|
6
|
+
wins and this page is the defect. The rule each section serves:
|
|
7
|
+
|
|
8
|
+
| Contract rule | Here | Kit (Python / Node) |
|
|
9
|
+
|---|---|---|
|
|
10
|
+
| LC-01 quit reaches exit within a deadline | [Shutdown](#shutdown) | `Drain` / `Drain` |
|
|
11
|
+
| LC-03 a job is bounded and exclusive | [one copy under launchd](#one-copy-under-launchd) | `hold_single_instance` / `holdSingleInstance` |
|
|
12
|
+
| LC-09 the footprint is declared | the descriptor and the plist; the service's own `AGENTS.md` names its label and port | `write_descriptor`, `launchd_plist` |
|
|
13
|
+
| LC-11 previous release kept, older pruned | [Install, upgrade, uninstall](#install-upgrade-uninstall) | `prune_releases` / `pruneReleases` |
|
|
14
|
+
| LC-12 every file is bounded | [Logs](#logs) | `RotatingLog`, `cap_stdout_log` / `RotatingLog`, `capStdoutLog` |
|
|
15
|
+
| LC-14 operator intent wins; uninstall is symmetric | [Install, upgrade, uninstall](#install-upgrade-uninstall) | `launchd_install`, `launchd_uninstall` (Python) |
|
|
16
|
+
| LC-15 builds clean up after themselves | release pruning runs inside the installer, not later | `prune_releases` / `pruneReleases` |
|
|
17
|
+
|
|
18
|
+
## Contents
|
|
19
|
+
|
|
20
|
+
- [launchd plist](#launchd-plist-generate-it-with-launchd_plist)
|
|
21
|
+
- [Install, upgrade, uninstall](#install-upgrade-uninstall)
|
|
22
|
+
- [One copy under launchd](#one-copy-under-launchd)
|
|
23
|
+
- [Directories](#directories)
|
|
24
|
+
- [Logs](#logs)
|
|
25
|
+
- [Shutdown](#shutdown)
|
|
26
|
+
- [Being managed by a host lifecycle broker](#being-managed-by-a-host-lifecycle-broker)
|
|
27
|
+
- [Linux](#linux)
|
|
28
|
+
|
|
3
29
|
## launchd plist (generate it with `launchd_plist`)
|
|
4
30
|
|
|
5
31
|
| Key | Value | Why |
|
|
@@ -9,9 +35,9 @@
|
|
|
9
35
|
| `RunAtLoad` | `true` | back at every login |
|
|
10
36
|
| `KeepAlive` | `true` | `{SuccessfulExit:false}` leaves a cleanly exited service down |
|
|
11
37
|
| `ThrottleInterval` | `10` | bounds a crash loop |
|
|
12
|
-
| `ExitTimeOut` | drain
|
|
38
|
+
| `ExitTimeOut` | above the drain and its hard exit (15 s default; `Drain` ends by 10 s) | SIGKILL arrives after it, so it must never come first |
|
|
13
39
|
| `ProcessType` | `Standard` — never `Background`, and no `Nice` > 0 or `LowPriorityIO` | a host probes the service and agents call it while the Mac is busy; a background job is starved for tens of seconds under load and every host reports an outage that never happened (the probe's `lifecycle.priority` rule fails it) |
|
|
14
|
-
| `EnvironmentVariables` | `PATH` set explicitly; `*_FILE` paths to secrets | never a secret value — the plist is world-readable in backups |
|
|
40
|
+
| `EnvironmentVariables` | `PATH` set explicitly; `*_FILE` paths to secrets; `FABRIC_SERVICE_SUPERVISOR=launchd` | never a secret value — the plist is world-readable in backups; the supervisor marker is how the lock knows to back off |
|
|
15
41
|
| `StandardOutPath`/`StandardErrorPath` | `~/Library/Logs/<id>/service.log` | one place the host tails |
|
|
16
42
|
|
|
17
43
|
## Install, upgrade, uninstall
|
|
@@ -19,17 +45,41 @@
|
|
|
19
45
|
1. Build or unpack the release into `~/.local/share/<id>/releases/<version>-<sha12>/`
|
|
20
46
|
(a venv or `node_modules` inside it). Never edit a release after it is written.
|
|
21
47
|
2. `write_descriptor` (port and id claims are checked here).
|
|
22
|
-
3. `launchd_install`: write + `plutil -lint`,
|
|
23
|
-
`
|
|
48
|
+
3. `launchd_install`: write + `plutil -lint`, read `launchctl print-disabled gui/<uid>`,
|
|
49
|
+
`enable` **only when no override is recorded** (a first install), `bootout` and wait
|
|
50
|
+
until `launchctl print gui/<uid>/<label>` fails, `bootstrap` with up to five retries,
|
|
24
51
|
then poll the well-known document until it answers with this `id.instance`.
|
|
25
52
|
4. Upgrade = new release, rewrite the plist with the new path, `launchd_install`
|
|
26
|
-
again
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
53
|
+
again, then `prune_releases(releases_dir, current=<new release>)`: the running release
|
|
54
|
+
and the one before it stay for rollback, older ones go (LC-11, LC-15).
|
|
55
|
+
5. Uninstall = `launchd_uninstall` + `remove_descriptor`. Data stays; `purge=True`
|
|
56
|
+
(with `service_id`) removes data, logs and cache when the operator asks.
|
|
57
|
+
|
|
58
|
+
**The operator's intent wins (LC-14).** A host stops a service with `bootout` **and**
|
|
59
|
+
`disable` (off survives a login) and starts it with `enable` + `bootstrap`; restart is
|
|
60
|
+
`kickstart -k`. An install or upgrade never re-enables a label the operator disabled: it
|
|
61
|
+
rewrites the plist, so the new release is what starts once they turn it back on, and
|
|
62
|
+
answers `{"disabled": true, ...}` instead of the well-known document. `force_enable=True`
|
|
63
|
+
is for an operator who asked for the service to be switched on. The service never
|
|
31
64
|
implements these verbs itself — its CLI calls launchctl the same way.
|
|
32
65
|
|
|
66
|
+
**Uninstall is symmetric.** `launchd_uninstall` boots the job out and **waits** until
|
|
67
|
+
launchd reports it gone (it raises, removing nothing, if the job is still loaded at the
|
|
68
|
+
timeout), removes the plist, and resets the override to `enabled`. launchctl has no verb
|
|
69
|
+
that deletes an override, and a leftover `disabled` would make a later fresh install stay
|
|
70
|
+
off. Tests use a throwaway label or a fake `launchctl` first on `PATH` — never the
|
|
71
|
+
operator's real jobs.
|
|
72
|
+
|
|
73
|
+
## One copy under launchd
|
|
74
|
+
|
|
75
|
+
Started by hand, a copy that finds `service.lock` held prints one sentence naming the
|
|
76
|
+
holder and exits 75 at once. Under launchd that exit is a respawn every `ThrottleInterval`
|
|
77
|
+
(10 s), forever. So a copy whose plist carries `FABRIC_SERVICE_SUPERVISOR=launchd` backs
|
|
78
|
+
off in-process — 0.5 s doubling to 30 s, 300 s in all — takes over if the holder leaves,
|
|
79
|
+
and exits 75 only when the wait runs out: a few starts an hour instead of 360. It touches
|
|
80
|
+
nothing while it waits. Two installs sharing one data directory are still the defect to
|
|
81
|
+
remove; the back-off only stops the loop from costing the machine.
|
|
82
|
+
|
|
33
83
|
## Directories
|
|
34
84
|
|
|
35
85
|
| What | macOS | Linux |
|
|
@@ -46,15 +96,28 @@ anything the operator would miss belongs in data.
|
|
|
46
96
|
|
|
47
97
|
## Logs
|
|
48
98
|
|
|
49
|
-
One structured log (JSON lines) rotated by size (
|
|
50
|
-
|
|
51
|
-
|
|
99
|
+
One structured log (JSON lines) rotated by size — `RotatingLog(dirs["logs"] / "service.jsonl")`,
|
|
100
|
+
5 × 5 MB by default, files 0600 in a 0700 directory — plus the launchd stdout file, which
|
|
101
|
+
`cap_stdout_log` copies to `.1` and truncates at start once it passes 5 MB (launchd holds it
|
|
102
|
+
open for append, so it is truncated in place, never renamed). Configure logging once, in the
|
|
103
|
+
entry point. Never log a token, a cookie, a login code or a request body that may carry one.
|
|
52
104
|
|
|
53
105
|
## Shutdown
|
|
54
106
|
|
|
55
107
|
On `SIGTERM`: stop accepting new work, append a `service.stopping` event, drain
|
|
56
|
-
in-flight work
|
|
57
|
-
|
|
108
|
+
in-flight work, persist what was interrupted so the next start resumes it, release the
|
|
109
|
+
lock, exit 0 — within 10 s with work in flight (LC-01). `Drain` does the timing:
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
drain = fs.Drain() # drain 8 s, hard exit 2 s later (EXIT_HARD_STOP, 70)
|
|
113
|
+
drain.install(lambda drained: server.shutdown(),
|
|
114
|
+
on_stopping=lambda: log.append("service.stopping", "info", "Stopping."))
|
|
115
|
+
with drain.work(): # raises fs.Stopping once a stop has begun -> answer 503
|
|
116
|
+
handle(request)
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Work longer than the drain is journalled before it starts and resumes on the next start;
|
|
120
|
+
it is never a reason to raise the deadline.
|
|
58
121
|
|
|
59
122
|
## Being managed by a host lifecycle broker
|
|
60
123
|
|
package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/protocol.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# fabric-service/0.1 — wire reference
|
|
2
2
|
|
|
3
|
-
Pinned to `fabric-agent-contract` commit `
|
|
4
|
-
(`docs/specification/service.md`, DEC-0015). The contract's schemas are normative;
|
|
3
|
+
Pinned to `fabric-agent-contract` commit `23f9fda4c05f8a3852246ee98d4f2adf74ed0875`
|
|
4
|
+
(`docs/specification/service.md`, DEC-0015; the usage report DEC-0021). The contract's schemas are normative;
|
|
5
5
|
this page is the working summary. Extension key:
|
|
6
6
|
`https://fabric.passioncode.ai/agent-contract/extensions/service/0.1`.
|
|
7
7
|
|
|
@@ -47,7 +47,8 @@ No auth; the Host/Origin guard applies; answered from memory in under 100 ms.
|
|
|
47
47
|
- `status`: `starting | ready | degraded | stopping`. No answer means `down`.
|
|
48
48
|
- `build` needs `commit` or `digest` (`sha256:<64 hex>`).
|
|
49
49
|
- `summary`: at most six tiles; `attention: true` counts toward the host's badge.
|
|
50
|
-
- `surfaces.events` is required; `dashboard` and `mcp` when the service has them
|
|
50
|
+
- `surfaces.events` is required; `dashboard` and `mcp` when the service has them; `usage`
|
|
51
|
+
(DEC-0021) when the service reports its spend — see [the usage reference](usage.md).
|
|
51
52
|
|
|
52
53
|
## Events page — `GET /fabric/v1/events?after=<cursor>&limit=<n>`
|
|
53
54
|
|
|
@@ -81,3 +82,4 @@ single use, at most 120 s, recorded as used before it is honoured. `GET` that UR
|
|
|
81
82
|
| `FAC-SEM-010` | no two descriptors claim one port or one `id.instance` |
|
|
82
83
|
| `FAC-SEM-011` | `ready` carries no degraded source |
|
|
83
84
|
| `FAC-SEM-012` | commands start with an absolute or `~/` executable |
|
|
85
|
+
| `FAC-SEM-025` | a usage report adds up; an all-unpriced row costs `null`, not `0` (DEC-0021) |
|
|
@@ -6,6 +6,12 @@
|
|
|
6
6
|
origin, stateless JSON responses unless the tools stream.
|
|
7
7
|
- Authenticate with the service token in the declared header. OAuth is for remote
|
|
8
8
|
multi-user servers, not for a loopback service.
|
|
9
|
+
- **Or keep the two roles apart (DEC-0024).** The descriptor's token is the host's: it reads
|
|
10
|
+
events and the usage report, and it mints operator login codes. An MCP token sits in every
|
|
11
|
+
client's `~/.claude.json`. When that matters, give agents their own token (`service.token`
|
|
12
|
+
for agents, `host.token` named by the descriptor), declare `surfaces.mcp.auth: "own"`, and
|
|
13
|
+
make `/mcp` refuse the host's token. `check_service.py` then checks only that refusal
|
|
14
|
+
(`interop.mcp-own-auth`) and leaves the caller rules NOT_RUN.
|
|
9
15
|
- Register it into a client config with the service's own command
|
|
10
16
|
(`<tool> mcp-register`), which writes `{"type":"http","url":"http://127.0.0.1:<port>/mcp","headers":{...}}`
|
|
11
17
|
atomically, aborts if the file changed meanwhile, and keeps one 0600 backup. The token
|
package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/usage.md
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Usage report — what the agent spent (DEC-0021)
|
|
2
|
+
|
|
3
|
+
Fabric Agent Contract DEC-0021, `docs/specification/service.md` → *Usage report*, schema
|
|
4
|
+
`schemas/service-usage.schema.json`. Optional. A service that calls paid models or tools should
|
|
5
|
+
offer it, so Fabric Dashboards (Spend, MCP `spend`) and Fabric can show each agent's spend beside
|
|
6
|
+
its health without knowing any provider.
|
|
7
|
+
|
|
8
|
+
## What to record
|
|
9
|
+
|
|
10
|
+
One **receipt per model call**, built from the numbers the provider returned:
|
|
11
|
+
|
|
12
|
+
```python
|
|
13
|
+
receipt = fs.make_usage_receipt("anthropic", "claude-sonnet-5-5",
|
|
14
|
+
input_tokens=u.input_tokens, output_tokens=u.output_tokens,
|
|
15
|
+
cache_read_tokens=u.cache_read_input_tokens or 0,
|
|
16
|
+
cost_usd=charged, cost_basis="provider") # or "price-list"
|
|
17
|
+
ledger.record(receipt) # fs.JsonlUsageLedger(data / "usage.jsonl")
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
```js
|
|
21
|
+
ledger.record(k.makeUsageReceipt('openrouter', model, { inputTokens, outputTokens, costUsd: usage.cost ?? null }));
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
- **The provider's own numbers.** Take tokens from the response's usage block, not from a local
|
|
25
|
+
estimate. The cost is `provider` when the provider reported the charge (OpenRouter `usage.cost`,
|
|
26
|
+
a billing API) and `price-list` when you computed it from a published price list.
|
|
27
|
+
- **Unknown is `None`/`null`, never `0`.** A local model, or a provider that reports no price,
|
|
28
|
+
records `cost_usd=None`. The report counts it in `unpricedCalls`, and a host shows a lower bound
|
|
29
|
+
(«≥ $x») or «unknown». A call that is really free (a template, a cached answer) records
|
|
30
|
+
`cost_usd=0.0` with `price-list`.
|
|
31
|
+
- **No content.** A receipt carries no prompt, output, caller or user id. The kits accept only the
|
|
32
|
+
fields above.
|
|
33
|
+
- If the service already keeps a ledger (a jobs table with token counts), build the receipts from
|
|
34
|
+
it and pass them to `usage_report(...)`. That avoids a second store.
|
|
35
|
+
|
|
36
|
+
## What to answer
|
|
37
|
+
|
|
38
|
+
Declare the surface in the well-known document and serve the report behind the service token:
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
surfaces={..., "usage": {"path": fs.USAGE_PATH}} # "/fabric/v1/usage"
|
|
42
|
+
# GET /fabric/v1/usage, token required:
|
|
43
|
+
return send(200, ledger.report(service_id=ID, instance=INSTANCE, budget={"period": "month", "limitUsd": 100}))
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
`usage_report` / `usageReport` give the last 31 UTC days, oldest first, per provider and model,
|
|
47
|
+
with day totals that are the sums of the model rows (`FAC-SEM-025`). Days without calls are
|
|
48
|
+
left out.
|
|
49
|
+
|
|
50
|
+
**Budget.** Pass `budget` only if the service **enforces** that limit itself. The report states
|
|
51
|
+
it, and the host only shows it.
|
|
52
|
+
|
|
53
|
+
Call `ledger.prune()` at start (or daily) to keep the file at the reported window (LC-12).
|
|
54
|
+
|
|
55
|
+
`JsonlUsageLedger` appends 0600 JSON lines. A line torn by a killed writer is skipped, and the
|
|
56
|
+
next receipt never glues onto it.
|
|
57
|
+
|
|
58
|
+
## Verify
|
|
59
|
+
|
|
60
|
+
`check_service.py <id>` reads a declared report: `usage.requires-token` (401 without the token)
|
|
61
|
+
and `usage.report` (identity, USD, dates forward, null for all-unpriced rows, sums). The
|
|
62
|
+
`sample_service.py` serves one. Fabric Dashboards → **Spend** shows it within a minute of opening
|
|
63
|
+
the page.
|