@ours.network/notifications 0.1.1

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.
Files changed (107) hide show
  1. package/LICENSE +98 -0
  2. package/README.md +231 -0
  3. package/dist/cli.js +446 -0
  4. package/dist/config.js +104 -0
  5. package/dist/daemon.js +353 -0
  6. package/dist/http.js +31 -0
  7. package/dist/log-store.js +48 -0
  8. package/dist/presence.js +157 -0
  9. package/dist/provision.js +29 -0
  10. package/dist/push.js +6 -0
  11. package/dist/web-push-sender.js +41 -0
  12. package/mufl_code/20E39399689579D5AFA5132C59DEDDD328C3BCA1A74D546F2193B36585DDD283.muflo +0 -0
  13. package/mufl_code/config.mufl +21 -0
  14. package/mufl_code/core/.github/FUNDING.yml +3 -0
  15. package/mufl_code/core/.github/workflows/docs.yml +39 -0
  16. package/mufl_code/core/.github/workflows/tests.yml +66 -0
  17. package/mufl_code/core/COMMERCIAL-LICENCE.md +25 -0
  18. package/mufl_code/core/COMPATIBILITY.md +221 -0
  19. package/mufl_code/core/CONTRIBUTING.md +37 -0
  20. package/mufl_code/core/LICENSE +98 -0
  21. package/mufl_code/core/NOTIFICATIONS_CONTRACT.md +290 -0
  22. package/mufl_code/core/README.md +91 -0
  23. package/mufl_code/core/SECURITY.md +40 -0
  24. package/mufl_code/core/TRADEMARKS.md +24 -0
  25. package/mufl_code/core/a2a_capabilities.mm +449 -0
  26. package/mufl_code/core/a2a_cluster.mm +601 -0
  27. package/mufl_code/core/a2a_control.mm +90 -0
  28. package/mufl_code/core/a2a_messaging.mm +4817 -0
  29. package/mufl_code/core/a2a_monitoring.mm +94 -0
  30. package/mufl_code/core/a2a_notification_integration.mm +144 -0
  31. package/mufl_code/core/a2a_notifications.mm +1127 -0
  32. package/mufl_code/core/a2a_protocol.mm +246 -0
  33. package/mufl_code/core/a2a_versions.mm +801 -0
  34. package/mufl_code/core/config.mufl +29 -0
  35. package/mufl_code/core/docs/.vitepress/config.mts +58 -0
  36. package/mufl_code/core/docs/citations.json +688 -0
  37. package/mufl_code/core/docs/guide/01-vendor-the-core.md +50 -0
  38. package/mufl_code/core/docs/guide/02-configure-and-compile.md +100 -0
  39. package/mufl_code/core/docs/guide/03-wire-the-host.md +154 -0
  40. package/mufl_code/core/docs/guide/04-connect-and-message.md +156 -0
  41. package/mufl_code/core/docs/guide/05-test-your-app.md +75 -0
  42. package/mufl_code/core/docs/guide/index.md +63 -0
  43. package/mufl_code/core/docs/how-it-works/capabilities-and-control.md +77 -0
  44. package/mufl_code/core/docs/how-it-works/cluster.md +65 -0
  45. package/mufl_code/core/docs/how-it-works/identity.md +56 -0
  46. package/mufl_code/core/docs/how-it-works/invites-and-contacts.md +76 -0
  47. package/mufl_code/core/docs/how-it-works/messaging.md +55 -0
  48. package/mufl_code/core/docs/how-it-works/monitoring-and-config.md +72 -0
  49. package/mufl_code/core/docs/how-it-works/overview.md +45 -0
  50. package/mufl_code/core/docs/how-it-works/versioning.md +116 -0
  51. package/mufl_code/core/docs/index.md +27 -0
  52. package/mufl_code/core/docs/package-lock.json +3467 -0
  53. package/mufl_code/core/docs/package.json +15 -0
  54. package/mufl_code/core/docs/reference/contributing.md +26 -0
  55. package/mufl_code/core/docs/reference/glossary.md +108 -0
  56. package/mufl_code/core/docs/reference/implementations.md +23 -0
  57. package/mufl_code/core/docs/reference/modules.md +19 -0
  58. package/mufl_code/core/docs/scripts/check-citations.mjs +36 -0
  59. package/mufl_code/core/docs/scripts/check-denylist.mjs +30 -0
  60. package/mufl_code/core/docs/scripts/gen-llms.mjs +32 -0
  61. package/mufl_code/core/docs/workflows/cluster.md +89 -0
  62. package/mufl_code/core/docs/workflows/contact-exchange.md +60 -0
  63. package/mufl_code/core/docs/workflows/contact-restore.md +60 -0
  64. package/mufl_code/core/docs/workflows/control-verbs.md +70 -0
  65. package/mufl_code/core/docs/workflows/index.md +57 -0
  66. package/mufl_code/core/docs/workflows/introductions.md +60 -0
  67. package/mufl_code/core/docs/workflows/messaging.md +75 -0
  68. package/mufl_code/core/docs/workflows/monitoring.md +84 -0
  69. package/mufl_code/core/docs/workflows/notifications.md +109 -0
  70. package/mufl_code/core/release-notes/0.1.md +60 -0
  71. package/mufl_code/core/release-notes/0.3.md +22 -0
  72. package/mufl_code/core/release-notes/0.4.md +97 -0
  73. package/mufl_code/core/tests/README.md +83 -0
  74. package/mufl_code/core/tests/compat/README.md +82 -0
  75. package/mufl_code/core/tests/compat/compat.mjs +245 -0
  76. package/mufl_code/core/tests/compat/compat_peer.mjs +146 -0
  77. package/mufl_code/core/tests/compat/run_compat.sh +96 -0
  78. package/mufl_code/core/tests/corpus.mjs +139 -0
  79. package/mufl_code/core/tests/corpus_actor.mu +242 -0
  80. package/mufl_code/core/tests/fatcompile_actor.mu +52 -0
  81. package/mufl_code/core/tests/mig.mjs +805 -0
  82. package/mufl_code/core/tests/mig_actor.mu +518 -0
  83. package/mufl_code/core/tests/migapp.mjs +431 -0
  84. package/mufl_code/core/tests/migapp_actor.mu +294 -0
  85. package/mufl_code/core/tests/mufl_semantics/run.sh +69 -0
  86. package/mufl_code/core/tests/mufl_semantics/t11_three_version.mufl +47 -0
  87. package/mufl_code/core/tests/mufl_semantics/t2_extra_fields.mufl +11 -0
  88. package/mufl_code/core/tests/mufl_semantics/t7_runtime.mufl +30 -0
  89. package/mufl_code/core/tests/mufl_semantics/t9_passthrough.mufl +19 -0
  90. package/mufl_code/core/tests/mufl_semantics/t_str_order.mufl +19 -0
  91. package/mufl_code/core/tests/notif.mjs +755 -0
  92. package/mufl_code/core/tests/notif_actor.mu +450 -0
  93. package/mufl_code/core/tests/protocol_container.mm +18 -0
  94. package/mufl_code/core/tests/run.sh +65 -0
  95. package/mufl_code/core/tests/run_corpus.sh +51 -0
  96. package/mufl_code/core/tests/run_fatcompile.sh +40 -0
  97. package/mufl_code/core/tests/run_mig.sh +51 -0
  98. package/mufl_code/core/tests/run_migapp.sh +51 -0
  99. package/mufl_code/core/tests/run_notif.sh +57 -0
  100. package/mufl_code/core/tests/test.mjs +750 -0
  101. package/mufl_code/core/tests/test_actor.mu +564 -0
  102. package/mufl_code/core/tests/test_common.mjs +93 -0
  103. package/mufl_code/core/version.mm +24 -0
  104. package/mufl_code/packet.mu +170 -0
  105. package/mufl_code/protocol_container.mm +18 -0
  106. package/package.json +43 -0
  107. package/scripts/gen-vapid.mjs +16 -0
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "ours-mufl-core-docs",
3
+ "private": true,
4
+ "type": "module",
5
+ "scripts": {
6
+ "docs:dev": "vitepress dev .",
7
+ "docs:build": "vitepress build .",
8
+ "docs:check": "node scripts/check-citations.mjs && node scripts/check-denylist.mjs"
9
+ },
10
+ "devDependencies": {
11
+ "mermaid": "^11.16.0",
12
+ "vitepress": "^1.6.0",
13
+ "vitepress-plugin-mermaid": "^2.0.17"
14
+ }
15
+ }
@@ -0,0 +1,26 @@
1
+ # Contributing
2
+
3
+ The full contribution guidelines are in
4
+ [`CONTRIBUTING.md`](https://github.com/adapt-toolkit/ours-mufl-core/blob/main/CONTRIBUTING.md)
5
+ in the repository root.
6
+
7
+ ## Current posture
8
+
9
+ Until the Contributor Licence Agreement (CLA) process is live, the project accepts
10
+ **issues and feedback only** — pull requests are not open. This applies to the protocol
11
+ repository. If you are integrating the core into your own agent, that integration is entirely
12
+ yours; no CLA or coordination is needed.
13
+
14
+ ## How to contribute
15
+
16
+ - **Bug reports and questions** — open an issue on the
17
+ [ours-mufl-core repository](https://github.com/adapt-toolkit/ours-mufl-core).
18
+ - **Feature discussion** — open an issue describing the use case. Protocol changes affect every
19
+ client, so discussion precedes any design.
20
+ - **Security issues** — do not open a public issue; see `SECURITY.md` in the repository root.
21
+ - **Code contributions** — on hold until the CLA is live. See `CONTRIBUTING.md` for the rationale.
22
+
23
+ ## Integration feedback
24
+
25
+ If you are building on the core and encounter something that does not match the docs, open an
26
+ issue. Integration experience is the most useful signal at this stage.
@@ -0,0 +1,108 @@
1
+ # Glossary
2
+
3
+ Terms used across the protocol documentation.
4
+ Source of truth: the `.mm` files and
5
+ [`README.md`](https://github.com/adapt-toolkit/ours-mufl-core/blob/main/README.md)
6
+ (the identical wire guarantee).
7
+
8
+ ---
9
+
10
+ **boxed send**
11
+ An encrypted message sent over a one-way ephemeral channel using a known public key, without a
12
+ pre-established `encrypted_channel`. Used during the invite redeem hop to carry identity
13
+ material before a persistent channel exists.
14
+ See [Invites & contacts](../how-it-works/invites-and-contacts.md).
15
+
16
+ **broker**
17
+ The relay service operated by ours.network that routes packets between agents. Agents do not
18
+ connect directly to each other; the broker forwards ciphertext without being able to read it.
19
+
20
+ **capability**
21
+ A named feature set advertised in an agent's app manifest. Each capability has a stable string
22
+ id (e.g. `"core.monitoring"`) and a schema; the control envelope routes requests to its handler
23
+ by `$cap` id.
24
+ See [Capabilities & control](../how-it-works/capabilities-and-control.md).
25
+
26
+ **cid / container\_id**
27
+ The stable, globally unique identifier for a MUFL packet instance (a running agent). Represented
28
+ as a `global_id`. Peers reference each other by container id in address documents and contact
29
+ registries.
30
+
31
+ **contact book**
32
+ The host-local registry of children and contacts maintained by the `core.cluster` capability
33
+ in `a2a_cluster.mm`. Entries record name, role, bio, persona, monitoring state, and capability
34
+ ids.
35
+ See [Cluster](../how-it-works/cluster.md).
36
+
37
+ **delegation cert**
38
+ A signed record (`delegation_cert_t`) that binds a role's container id and address-document hash
39
+ to its root, signed by the root's keys. A root identity carries no delegation cert (`NIL`);
40
+ detection is structural.
41
+ See [Identity: roots & roles](../how-it-works/identity.md).
42
+
43
+ **encrypted channel**
44
+ The persistent, per-contact end-to-end-encrypted session established during invite redemption and
45
+ restored from stored peer address documents on restart. All message and file traffic rides this
46
+ channel.
47
+ See [Messaging](../how-it-works/messaging.md).
48
+
49
+ **envelope**
50
+ A `control_envelope_t` record (`$cap`, `$verb`, `$args`, `$req_id`) that carries a capability
51
+ request. All inbound capability requests arrive in this shape; dispatch keys on `$cap`.
52
+ See [Capabilities & control](../how-it-works/capabilities-and-control.md).
53
+
54
+ **hidden state**
55
+ A MUFL field declared `hidden` that only code inside the declaring library can write. Used for
56
+ `monitoring_proxy` and `app_config` in `a2a_messaging.mm` to prevent any external library or
57
+ app from suppressing forced copies or overwriting configuration.
58
+ See [Monitoring & config](../how-it-works/monitoring-and-config.md).
59
+
60
+ **introduction**
61
+ A contact-establishment shortcut via a shared control plane. The CP sends each side the other's
62
+ signed address document via `ingest_connect_descriptor`; no out-of-band invite is needed.
63
+ Requires both nodes to advertise `core.connect`.
64
+ See [Invites & contacts](../how-it-works/invites-and-contacts.md).
65
+
66
+ **invite leg**
67
+ One step of the invite redeem flow (OOB transfer, leg 1, leg 2, leg 3). Leg 1 is the responder's
68
+ BARE send to the inviter; leg 2 is the inviter's receipt and verification of leg 1 (consuming the
69
+ invite and emitting leg 3); leg 3 is the inviter's BARE send reply to the responder.
70
+ See [Invites & contacts](../how-it-works/invites-and-contacts.md).
71
+
72
+ **MAJ / MIN**
73
+ The major (`$MAJ`) and minor (`$MIN`) components of `version_t`. A `$MAJ` bump signals breaking
74
+ changes; a `$MIN` bump signals new features with no removals.
75
+ See [Versioning](../how-it-works/versioning.md).
76
+
77
+ **monitoring copy**
78
+ A forced encrypted copy of every inbound and outbound message sent to the bound control plane
79
+ when `monitoring_proxy` is set. Generated by `monitor_copy_actions` in `a2a_messaging.mm`;
80
+ received by `a2a_monitoring.mm` on the CP side.
81
+ See [Monitoring & config](../how-it-works/monitoring-and-config.md).
82
+
83
+ **packet**
84
+ A compiled MUFL application instance — the unit that runs as an agent. Consumers vendor the
85
+ core and compile it together with their own libraries into a packet. Every packet that links the
86
+ core speaks the same wire format.
87
+
88
+ **role**
89
+ A delegated identity anchored to a root. A role carries a `delegation_cert_t` signed by its
90
+ root. Roles are the typical identity type for child/subagents in a cluster.
91
+ See [Identity: roots & roles](../how-it-works/identity.md).
92
+
93
+ **root**
94
+ A self-sovereign keypair identity with no delegation cert (`NIL`). A root is the anchor of an
95
+ identity hierarchy; roles are derived from it.
96
+ See [Identity: roots & roles](../how-it-works/identity.md).
97
+
98
+ **verb**
99
+ The operation requested within a capability, carried in the `$verb` field of a
100
+ `control_envelope_t`. Verbs must be consciously listed in `control_auth_class` to become
101
+ reachable through dispatch.
102
+ See [Capabilities & control](../how-it-works/capabilities-and-control.md).
103
+
104
+ **wrapper**
105
+ The host-side shim that connects the MUFL packet to the node runtime: wires the
106
+ `protocol_container`, routes inbound messages to `::actor::receive_message`, and marshals
107
+ notify-actions to host primitives.
108
+ See [Wire the host](../guide/03-wire-the-host.md).
@@ -0,0 +1,23 @@
1
+ # Reference implementations
2
+
3
+ These two applications are the living examples of everything in the
4
+ [integration guide](../guide/index.md): real agents built on the core, each vendoring this repo
5
+ as a git submodule. Read their `config.mufl`, host wiring, and tests — not this page — for the
6
+ details.
7
+
8
+ ## ours-mcp
9
+
10
+ [**ours-mcp**](https://github.com/adapt-toolkit/ours-mcp) is the MCP agent server. It vendors
11
+ the core at `packages/core/mufl_code/core`. Its `config.mufl` shows how the core export merges
12
+ with the packet's own libraries; its host wiring shows how the MUFL actor connects to the node
13
+ runtime; its tests show end-to-end protocol flows in production use.
14
+
15
+ ## ours-tg-connector
16
+
17
+ [**ours-tg-connector**](https://github.com/adapt-toolkit/ours-tg-connector) is the Telegram
18
+ connector. It vendors the core at `mufl_code/core`. The same integration pattern —
19
+ `config_load #"core"`, host wiring, test suite — applied to a different transport.
20
+
21
+ ---
22
+
23
+ The guide describes the pattern. These repos are the pattern applied.
@@ -0,0 +1,19 @@
1
+ # Modules
2
+
3
+ The core is eight `.mm` libraries and one config export loaded by `config_load #"core"` from a consumer's
4
+ `config.mufl`. The source is the authoritative reference; the
5
+ [how-it-works](../how-it-works/overview.md) pages explain the protocol design behind each module.
6
+
7
+ | File | Purpose |
8
+ |------|---------|
9
+ | [`a2a_capabilities.mm`](https://github.com/adapt-toolkit/ours-mufl-core/blob/main/a2a_capabilities.mm) | App manifest, capability/verb envelope and dispatch, well-known capability ids (`core.configuration`, `core.monitoring`, `core.connect`, `core.cluster`). |
10
+ | [`a2a_protocol.mm`](https://github.com/adapt-toolkit/ours-mufl-core/blob/main/a2a_protocol.mm) | Wire-facing shapes (invites, delegation certificates, root profiles, contact roots, introduction credentials) and the shared verification helpers. |
11
+ | [`a2a_messaging.mm`](https://github.com/adapt-toolkit/ours-mufl-core/blob/main/a2a_messaging.mm) | Contact and messaging transactions: invite generation/redemption, add/remove contact, send message, send file, inbound receive, and the introduction flow. |
12
+ | [`a2a_cluster.mm`](https://github.com/adapt-toolkit/ours-mufl-core/blob/main/a2a_cluster.mm) | The `core.cluster` capability handler: child/subagent lifecycle, per-child monitoring authorization, host-local contact book, and introductions between children and contacts. |
13
+ | [`a2a_monitoring.mm`](https://github.com/adapt-toolkit/ours-mufl-core/blob/main/a2a_monitoring.mm) | Control-plane receiver side of monitoring copies: validates sender and hands the copy to the app's storage hook. |
14
+ | [`a2a_control.mm`](https://github.com/adapt-toolkit/ours-mufl-core/blob/main/a2a_control.mm) | Control-plane transport: an opaque payload delivered to a contact over the `encrypted_channel`, validated on receipt. |
15
+ | [`a2a_notifications.mm`](https://github.com/adapt-toolkit/ours-mufl-core/blob/main/a2a_notifications.mm) | Notification service protocol: per-contact scoped tokens (`$scope`), registration and WebPush bindings, token issuance/rotation/revocation, receive-mute, and the bare signed-send notification ingest. |
16
+ | [`version.mm`](https://github.com/adapt-toolkit/ours-mufl-core/blob/main/version.mm) | Core version record, readable at runtime via `get_core_version`. |
17
+ | [`config.mufl`](https://github.com/adapt-toolkit/ours-mufl-core/blob/main/config.mufl) | Compile configuration: exports the libraries above for `config_load #"core"`. |
18
+
19
+ Source: [`README.md`](https://github.com/adapt-toolkit/ours-mufl-core/blob/main/README.md).
@@ -0,0 +1,36 @@
1
+ // Validates docs/citations.json: every cited file exists in the repo,
2
+ // every cited symbol greps in that file, every cited page exists,
3
+ // and every non-stub content page has at least one citation entry.
4
+ import { readFileSync, existsSync, readdirSync, statSync } from 'node:fs'
5
+ import { join, dirname } from 'node:path'
6
+ import { fileURLToPath } from 'node:url'
7
+
8
+ const docsRoot = join(dirname(fileURLToPath(import.meta.url)), '..')
9
+ const repoRoot = join(docsRoot, '..')
10
+ const manifest = JSON.parse(readFileSync(join(docsRoot, 'citations.json'), 'utf8'))
11
+ const errors = []
12
+
13
+ for (const [page, cites] of Object.entries(manifest)) {
14
+ if (!existsSync(join(docsRoot, page))) errors.push(`cited page missing: ${page}`)
15
+ for (const { file, symbol } of cites) {
16
+ const target = join(repoRoot, file)
17
+ if (!existsSync(target)) { errors.push(`${page}: cited file missing: ${file}`); continue }
18
+ if (symbol && !readFileSync(target, 'utf8').includes(symbol))
19
+ errors.push(`${page}: symbol "${symbol}" not found in ${file}`)
20
+ }
21
+ }
22
+
23
+ // coverage: every content page that is no longer a stub must be in the manifest
24
+ const stubMarker = '*(content lands in Tasks'
25
+ for (const dir of ['how-it-works', 'workflows', 'guide', 'reference']) {
26
+ for (const f of readdirSync(join(docsRoot, dir))) {
27
+ if (!f.endsWith('.md')) continue
28
+ const rel = `${dir}/${f}`
29
+ const body = readFileSync(join(docsRoot, rel), 'utf8')
30
+ if (!body.includes(stubMarker) && !(rel in manifest))
31
+ errors.push(`content page has no citations registered: ${rel}`)
32
+ }
33
+ }
34
+
35
+ if (errors.length) { console.error('CITATION CHECK FAILED\n' + errors.join('\n')); process.exit(1) }
36
+ console.log('citation check OK:', Object.keys(manifest).length, 'pages')
@@ -0,0 +1,30 @@
1
+ // Internal team-process artifacts, host paths, and unmerged-feature references
2
+ // must never ship (SPEC C1/C2/C6).
3
+ import { readFileSync, readdirSync, statSync } from 'node:fs'
4
+ import { join, dirname, relative } from 'node:path'
5
+ import { fileURLToPath } from 'node:url'
6
+
7
+ // Patterns are assembled from fragments so this file itself never matches the
8
+ // deny-list checker (a literal match in scripts/ would be a false positive).
9
+ const DENY = [
10
+ new RegExp('WS-' + 'B'),
11
+ new RegExp('WS-' + 'C'),
12
+ new RegExp('WS-' + 'D'),
13
+ new RegExp('\\bcritic\\b', 'i'),
14
+ new RegExp('TEAM-' + 'PROTOCOL'),
15
+ new RegExp('Coordinator-' + 'frozen'),
16
+ new RegExp('/home/' + 'fleet'),
17
+ ]
18
+ const docsRoot = join(dirname(fileURLToPath(import.meta.url)), '..')
19
+ const errors = []
20
+ const walk = d => { for (const e of readdirSync(d)) {
21
+ const p = join(d, e)
22
+ if (e === 'node_modules' || e === '.vitepress' || e === 'scripts') continue
23
+ if (statSync(p).isDirectory()) walk(p)
24
+ else if (e.endsWith('.md')) for (const term of DENY)
25
+ if (term.test(readFileSync(p, 'utf8')))
26
+ errors.push(`${relative(docsRoot, p)}: matches ${term}`)
27
+ }}
28
+ walk(docsRoot)
29
+ if (errors.length) { console.error('DENYLIST CHECK FAILED\n' + errors.join('\n')); process.exit(1) }
30
+ console.log('denylist check OK')
@@ -0,0 +1,32 @@
1
+ // Emits llms.txt (annotated index) and llms-full.txt (all pages concatenated)
2
+ // into .vitepress/dist after a build. Order follows the sidebar sections.
3
+ import { readFileSync, writeFileSync, existsSync } from 'node:fs'
4
+ import { join, dirname } from 'node:path'
5
+ import { fileURLToPath } from 'node:url'
6
+
7
+ const docsRoot = join(dirname(fileURLToPath(import.meta.url)), '..')
8
+ const dist = join(docsRoot, '.vitepress/dist')
9
+ const SITE = 'https://adapt-toolkit.github.io/ours-mufl-core'
10
+ const ORDER = ['index.md',
11
+ ...['overview','identity','invites-and-contacts','messaging','capabilities-and-control',
12
+ 'cluster','monitoring-and-config','versioning'].map(p => `how-it-works/${p}.md`),
13
+ ...['index','contact-exchange','messaging','contact-restore','monitoring',
14
+ 'control-verbs','introductions','cluster'].map(p => `workflows/${p}.md`),
15
+ ...['index','01-vendor-the-core','02-configure-and-compile','03-wire-the-host',
16
+ '04-connect-and-message','05-test-your-app'].map(p => `guide/${p}.md`),
17
+ ...['modules','implementations','glossary','contributing'].map(p => `reference/${p}.md`)]
18
+
19
+ let index = `# ours-mufl-core — agent docs index\n\n`
20
+ let full = ''
21
+ for (const rel of ORDER) {
22
+ const p = join(docsRoot, rel)
23
+ if (!existsSync(p)) continue
24
+ const body = readFileSync(p, 'utf8')
25
+ const title = (body.match(/^#\s+(.+)$/m) ?? [,rel])[1]
26
+ const url = `${SITE}/${rel.replace(/index\.md$/, '').replace(/\.md$/, '')}`
27
+ index += `- [${title}](${url})\n`
28
+ full += `\n\n----- ${rel} -----\n\n${body}`
29
+ }
30
+ writeFileSync(join(dist, 'llms.txt'), index)
31
+ writeFileSync(join(dist, 'llms-full.txt'), full.trimStart())
32
+ console.log('llms.txt + llms-full.txt written')
@@ -0,0 +1,89 @@
1
+ # Cluster lifecycle
2
+
3
+ `core.cluster` verbs manage hosted children (subagents) of a root identity. Operations that
4
+ need host work — provisioning a packet, destroying one, minting a child's invite, binding a
5
+ child's monitoring — are **asynchronous**: the verb handler validates, persists a pending
6
+ request keyed by a unique handle, emits a *host-primitive* notify to the local daemon, and
7
+ acknowledges `$pending` immediately. The daemon does the work and calls back a host-only
8
+ transaction that consumes the pending request and routes the real result to the original
9
+ controller.
10
+
11
+ Traced from [`a2a_cluster.mm`](https://github.com/adapt-toolkit/ours-mufl-core/blob/main/a2a_cluster.mm)
12
+ (`cluster_handler`, `register_provisioned_child`, `sweep_and_settle`, `reconcile`) and
13
+ [`a2a_messaging.mm`](https://github.com/adapt-toolkit/ours-mufl-core/blob/main/a2a_messaging.mm)
14
+ (`relay_enroll_delegated_node`, `handle_enroll_delegated_node`).
15
+
16
+ ## Async child create
17
+
18
+ ```mermaid
19
+ sequenceDiagram
20
+ autonumber
21
+ participant C as Controller (control plane)
22
+ participant R as Root packet
23
+ participant D as Root host (daemon)
24
+ participant K as New child packet
25
+
26
+ C->>R: control_message - core.cluster / create ($name, $bio, $req_id)
27
+ Note over R: dispatch authz - cluster_handler - duplicate check<br/>(registry AND pending creates, name is a global op-key)
28
+ Note over R: pending_reqs[handle] = (sender, $req_id, verb, op-key) -<br/>pending_create_names[$name]
29
+ R-->>D: notify host_provision_child ($name, $bio, $pending_handle)
30
+ R-->>C: immediate ack: $ok TRUE, $pending TRUE
31
+
32
+ Note over D: provision the child packet + delegate its role
33
+ D->>K: create the packet (host runtime op)
34
+ D->>R: register_provisioned_child ($pending_handle, $role_id, $child_ad)
35
+ Note over R: host-only gates: origin user + handle must match an<br/>outstanding create - consumed atomically (unsolicited callbacks abort)
36
+ Note over R: cluster_members[cid] registered - caps seeded from my own manifest
37
+ R-->>D: async reply routed to the STORED controller ($target, $response)
38
+ D->>C: response: $cid, $name, $monitoring "off" (correlated by $req_id)
39
+ R->>C: receive_roster_update ($version, $members) - push, if a CP is bound
40
+ ```
41
+
42
+ `remove` (destroy + `confirm_child_destroyed`), `contact` (mint the **child's** invite in the
43
+ child's own packet + `register_child_invite` — a root-minted invite would connect the caller to
44
+ the root, not the child), and `set_monitoring` (host-bind or host-clear the child's proxy +
45
+ `confirm_child_monitoring`) follow the same handle pattern with their own host primitives.
46
+
47
+ Two safety nets close the loop:
48
+
49
+ - **`reconcile`** — host truth joined with the registry: backfills children created
50
+ out-of-band, drops members no longer hosted, preserves CP-authoritative fields (`$bio`,
51
+ `$monitoring`) for existing members.
52
+ - **`sweep_and_settle`** — pending requests older than 120 s are settled: a create whose name
53
+ now exists is adopted as success, otherwise timed out; an expired create also clears its
54
+ global name key so a lost callback never leaves a name permanently un-creatable.
55
+
56
+ ## Cluster enrollment (one root bind conveys the whole cluster)
57
+
58
+ The control plane holds `peer_ads` for every cluster member off a **single root bind**: the
59
+ root relays each child's public material; the child never participates.
60
+
61
+ ```mermaid
62
+ sequenceDiagram
63
+ autonumber
64
+ participant D as Root host
65
+ participant R as Root packet
66
+ participant CP as Control-plane packet
67
+
68
+ D->>R: relay_enroll_delegated_node ($proxy, $child_ad, $delegation_cert, $root_profile)
69
+ Note over R: root-only (a role cannot relay)
70
+ R->>CP: enroll_delegated_node ($child_ad, $delegation_cert, $root_profile) - encrypted channel
71
+ Note over CP: gates: child document self-signature (proof-of-possession) -<br/>verify_peer_delegation binds child cid + document hash to a root,<br/>root-signed - the SENDER must BE that root<br/>(possession of a valid chain is not enough)
72
+ Note over CP: contact registered as root_name/role_id -<br/>re-enroll just refreshes the stored document
73
+ CP-->>CP: notify $enrolled (or $reenrolled) to its host
74
+ ```
75
+
76
+ ## Per-child monitoring, derived — never named
77
+
78
+ On `set_monitoring` enable, the CP a child gets bound to is **derived from the root's own
79
+ ceremony-pinned `monitoring_proxy`** (`bound_cp_cid`), never taken from the request arguments —
80
+ so a child can only ever be bound to the CP its root actually ceremonied. The daemon first
81
+ host-injects the CP as a contact into the child (`host_register_monitoring_cp`, with the CP
82
+ document verified), because a network introduction would be rejected by the child's CP-only
83
+ acceptance gate and would race the ceremony. Disable host-clears the child's proxy and drops
84
+ the injected CP contact (`host_clear_child_monitoring`).
85
+
86
+ Roster changes (create, remove, bio/persona updates, monitoring flips, reconcile diffs) push a
87
+ sequenced snapshot to the bound CP — `receive_roster_update`, ingested on the CP side by the
88
+ `on_roster_update` hook (see
89
+ [`a2a_monitoring.mm`](https://github.com/adapt-toolkit/ours-mufl-core/blob/main/a2a_monitoring.mm)).
@@ -0,0 +1,60 @@
1
+ # Contact exchange (invite redeem)
2
+
3
+ Two strangers become mutual contacts through a **slim ephemeral-key invite** and a three-leg
4
+ redeem handshake. The invite blob itself carries no identity material — only an ephemeral
5
+ pubkey and correlation data (see [Invites & contacts](../how-it-works/invites-and-contacts.md)
6
+ for the invite shape). Identity bundles move inside boxes on legs 1 and 3, both of which are
7
+ **bare sends**: the two sides are not each other's contacts yet, so the encrypted channel
8
+ cannot carry them.
9
+
10
+ Traced from [`a2a_messaging.mm`](https://github.com/adapt-toolkit/ours-mufl-core/blob/main/a2a_messaging.mm)
11
+ (`generate_invite` / `mint_eph_invite`, `add_contact`, `handle_submit_invite_response`,
12
+ `handle_complete_invite`).
13
+
14
+ ```mermaid
15
+ sequenceDiagram
16
+ autonumber
17
+ participant IH as Inviter host
18
+ participant I as Inviter packet
19
+ participant R as Responder packet
20
+ participant RH as Responder host
21
+
22
+ IH->>I: generate_invite ($name)
23
+ Note over I: mint_eph_invite - fresh ephemeral keypair.<br/>pending_invites[id] = pub half + assigned name.<br/>pending_invite_keys[id] = private half (hidden, never exported)
24
+ I-->>IH: $invite blob, $invite_id
25
+
26
+ Note over IH,RH: invite blob travels out-of-band (QR, link, chat)
27
+
28
+ RH->>R: add_contact ($invite, $name)
29
+ Note over R: LEG 1 - fresh responder ephemeral keypair.<br/>Box my identity bundle to the invite's eph pubkey.<br/>pending_redemptions[id] + pending_redemption_keys[id]
30
+ R->>I: submit_invite_response ($invite_id, $epk, $v, $data) - bare boxed send
31
+
32
+ Note over I: LEG 2 gates, in order, no writes until all pass:<br/>pending lookup (single-use) - box-open -<br/>cid-bind + PoP self-sig - optional delegation chain
33
+ Note over I: register contact + peer_ads, consume<br/>pending_invites + pending_invite_keys atomically
34
+ I->>R: complete_invite ($invite_id, $epk, $v, $data) - bare boxed send
35
+ I-->>IH: notify $contact_accepted
36
+
37
+ Note over R: LEG 3 gates: pending lookup - expected-inviter<br/>cid pin - box-open with kept eph key -<br/>cid-bind + PoP - optional chain
38
+ Note over R: register contact + peer_ads, clear<br/>pending_redemptions + pending_redemption_keys
39
+ R-->>RH: notify $contact_added
40
+
41
+ Note over I,R: both sides registered - encrypted_channel carries all further traffic
42
+ ```
43
+
44
+ ## Key properties visible in the flow
45
+
46
+ - **Single-use**: the first valid leg 2 consumes `pending_invites[id]` *and*
47
+ `pending_invite_keys[id]` together. A replayed leg 1 aborts with `already-redeemed` and
48
+ mutates nothing; a leg 1 that fails a gate (bad box, forged bundle) consumes nothing.
49
+ - **Disclosure order**: the responder discloses its identity first (leg 1); the inviter answers
50
+ with its own bundle only after the responder's bundle verified (leg 3).
51
+ - **Why bare sends**: on leg 1 the inviter is not registered on the responder side (and vice
52
+ versa on leg 3), so `send_encrypted_tx` could not resolve a source key. The box to the
53
+ ephemeral key is the confidentiality; envelope signing plus the cid-bind and
54
+ proof-of-possession checks are the authenticity.
55
+ - **Role invites**: when either side is a delegated role, its bundle also carries the delegation
56
+ cert, root profile, and optional root-CP binding — verified with `verify_identity_bundle`, so
57
+ each side learns the other's verified root linkage (`contact_roots`).
58
+
59
+ An invite can also be minted for a hosted child via the cluster `contact` verb — same
60
+ construction path (`mint_eph_invite`), see [Cluster lifecycle](./cluster.md).
@@ -0,0 +1,60 @@
1
+ # Contact restore
2
+
3
+ A **degraded contact** is a cid present in `contacts` but missing from `peer_ads` — typically
4
+ after a breaking-change migration carried the contact but had to drop its address document.
5
+ Contact restore re-runs the key exchange between two *mutually known* addresses: the same
6
+ machinery as the invite legs (identity bundle, box to an ephemeral key, gates before any
7
+ write), but the trust anchor is "a signed request from an address already in my contacts"
8
+ instead of an out-of-band invite token.
9
+
10
+ Traced from [`a2a_messaging.mm`](https://github.com/adapt-toolkit/ours-mufl-core/blob/main/a2a_messaging.mm)
11
+ (`begin_contact_restore`, `handle_request_contact_restore`, `handle_submit_restore_response`,
12
+ `handle_complete_restore`, `flush_deferred`, `restore_degraded_contacts`).
13
+
14
+ ```mermaid
15
+ sequenceDiagram
16
+ autonumber
17
+ participant H as Requester host
18
+ participant A as Requester packet (degraded contact for B)
19
+ participant B as Responder packet (still holds A in contacts)
20
+
21
+ Note over H,A: trigger: send_message toward the degraded contact<br/>(queues into deferred_msgs) OR the host boot/GC sweep<br/>restore_degraded_contacts (up to 30 attempts per contact)
22
+ Note over A: begin_contact_restore - fresh ephemeral keypair + $rid.<br/>pending_restores[B] (replaces any outstanding attempt)
23
+ A->>B: LEG 0 request_contact_restore ($rid, $epk, $v) - bare signed send
24
+
25
+ Note over B: gate: sender in contacts - else SILENT no-op<br/>(no error reply, so address knowledge never leaks)
26
+ Note over B: pending_restore_replies[A] + fresh reply eph keypair.<br/>Nothing installed or replaced yet
27
+ B->>A: LEG 1 submit_restore_response ($rid, $epk, $v, $data) - B's bundle boxed to A's eph key
28
+
29
+ Note over A: gates: pending lookup - $rid pin - box-open -<br/>cid-bind + PoP - optional chain
30
+ Note over A: reinstall peer_ads[B], consume pending_restores single-use
31
+ A->>B: LEG 2 complete_restore ($rid, $epk, $v, $data) - A's bundle boxed to B's reply eph key
32
+ A-->>H: notify $contact_restored
33
+
34
+ Note over B: gates: reply lookup - $rid pin - box-open - cid-bind + PoP - chain
35
+ Note over B: REPLACE peer_ads[A] (a reseeded peer rolls fresh keys,<br/>so even a present-but-stale document must be replaced)
36
+ B-->>B: notify $contact_restored (to its own host)
37
+
38
+ H->>A: flush_deferred ($contact) - host-driven on the notify
39
+ A->>B: receive_message x queued - encrypted channel, original wire_ids preserved
40
+ ```
41
+
42
+ ## Why the pieces are shaped this way
43
+
44
+ - **Leg 0 is unboxed** (just `$rid`, an ephemeral pubkey, and a scheme id): there is nothing
45
+ secret to carry yet, and the framework signs every envelope, so the responder authenticates
46
+ the requester from the envelope alone.
47
+ - **Silent no-op for strangers**: a request from an address not in `contacts` returns success
48
+ with no actions — whether an address is known never leaks.
49
+ - **The flush is host-driven**, not automatic: firing `flush_deferred` on the
50
+ `$contact_restored` notify means the encrypted sends can never race the restore legs' bare
51
+ sends on the wire.
52
+ - **Retry budget**: the host sweep re-fires on its GC cadence, up to `restore_max_attempts`
53
+ (30) per contact; a peer that upgraded and came back online answers on the first
54
+ post-upgrade attempt. Each re-mint supersedes the previous ephemeral key, so a stale leg-1
55
+ reply fails both the `$rid` check and the unbox.
56
+ - **Observability**: `list_degraded_contacts` and `list_deferred_queues` are the readonly
57
+ views the host sweep keys off.
58
+
59
+ This flow is what makes contacts survive breaking changes — see the migration contract in
60
+ [Versioning](../how-it-works/versioning.md).
@@ -0,0 +1,70 @@
1
+ # Control-plane verb calls
2
+
3
+ All capability verbs — configuration, monitoring, connect, cluster — ride **one** transport
4
+ transaction (`a2a_control`'s `control_message`) carrying a typed envelope
5
+ `($cap, $verb, $args, $req_id)`. The receiving side routes it through a single dispatch
6
+ chokepoint with a fail-closed authorization table. MUFL has no JSON codec, so the daemon
7
+ adapts JSON to native records on the way in and marshals the native response envelope back to
8
+ JSON on the way out — generically, with no per-verb logic in the host.
9
+
10
+ Traced from [`a2a_control.mm`](https://github.com/adapt-toolkit/ours-mufl-core/blob/main/a2a_control.mm)
11
+ (`send_control`, `control_message`),
12
+ [`a2a_capabilities.mm`](https://github.com/adapt-toolkit/ours-mufl-core/blob/main/a2a_capabilities.mm)
13
+ (`dispatch`, `control_auth_class`), and
14
+ [`a2a_messaging.mm`](https://github.com/adapt-toolkit/ours-mufl-core/blob/main/a2a_messaging.mm)
15
+ (`authorize_control`).
16
+
17
+ ```mermaid
18
+ sequenceDiagram
19
+ autonumber
20
+ participant C as Controller node (e.g. messenger)
21
+ participant T as Target packet
22
+ participant D as Target host (daemon)
23
+
24
+ C->>T: send_control - control_message ($payload JSON, $app_id) - encrypted channel
25
+ Note over T: gates: external + encrypted + sender is a known contact
26
+ T-->>D: on_control_received hook ($sender_id, $payload, $app_id, $date)
27
+ Note over D: adapt: parse $payload JSON into a native envelope<br/>($cap, $verb, $args, $req_id)
28
+ D->>T: a2a_capabilities::dispatch (sender, envelope)
29
+ Note over T: control_auth_class ($cap, $verb) - fail-closed:<br/>public - pass / bootstrap - pass (bind checks its own code) /<br/>controller - authorize_control: sender must BE the bound<br/>monitoring proxy / anything unlisted - deny
30
+ alt authorized
31
+ Note over T: handler for $cap runs (a2a_cluster::cluster_handler,<br/>monitoring_handler, connect_handler, app handlers)
32
+ T-->>D: response_envelope ($req_id, $ok, $result, $err) as return_data
33
+ else denied
34
+ T-->>D: response_envelope $ok FALSE ($err unknown_verb / unauthorized)
35
+ end
36
+ Note over D: marshal the native envelope to JSON
37
+ D->>T: send_control back toward the controller
38
+ T->>C: control_message (response payload)
39
+ Note over C: correlate on (sender, $req_id)
40
+ ```
41
+
42
+ ## The authorization classes
43
+
44
+ `control_auth_class` is a pure table in the lowest layer; the stateful half
45
+ (`authorize_control`, which reads the hidden `monitoring_proxy`) is wired in at init and
46
+ enforced *inside* `dispatch`, so an app cannot wire routing while forgetting the gate — a
47
+ controller-class verb with no authorizer wired aborts.
48
+
49
+ | Class | Verbs | Who may call |
50
+ |-------|-------|--------------|
51
+ | `public` | none via dispatch (`get_manifest` is a standalone readonly transaction, never routed through dispatch) | anyone |
52
+ | `bootstrap` | `core.monitoring` / `bind` | whoever presents the 6-digit code (pre-bind by definition) |
53
+ | `controller` | the explicit list: `core.cluster` verbs, `core.monitoring` / `disable`, `core.connect` / `introduce`, `core.configuration` `get_config` / `set_config` | only the bound control plane |
54
+ | `deny` | everything else | no one — a new verb must be consciously classified to become reachable |
55
+
56
+ ## Async verbs
57
+
58
+ Some cluster verbs cannot complete inside one transaction (provisioning a child takes host
59
+ work). Their handlers return an immediate `$pending` acknowledgment and the real result is
60
+ routed later by a host callback to the *stored* original controller — see
61
+ [Cluster lifecycle](./cluster.md) for that pattern.
62
+
63
+ ## Configuration writes
64
+
65
+ `set_app_config` follows the same trust rule as every controller-class verb: external,
66
+ encrypted, sender must be the bound CP (`require_bound_cp_or_abort`). The blob is opaque to the
67
+ core — a `$config_updated` notify wakes the host wrapper, which pulls it via `get_app_config`
68
+ and applies the operational parts. See
69
+ [Capabilities & control](../how-it-works/capabilities-and-control.md) for the envelope types
70
+ and manifest shape.
@@ -0,0 +1,57 @@
1
+ # Transaction flows
2
+
3
+ Each page in this section traces **one conceptual protocol workflow** as a sequence diagram:
4
+ which transaction invokes which, how the call leaves the node, and how the result comes back.
5
+ Every arrow is labeled with the real transaction or function name from the `.mm` source — the
6
+ diagrams are traced from the code, not from a design document.
7
+
8
+ ## How to read the diagrams
9
+
10
+ **Participants.** A *packet* is the compiled MUFL protocol container of one identity. A *host*
11
+ is the daemon embedding that packet (the reference host is the ours MCP daemon). A *control
12
+ plane* (CP) is a peer node bound via the monitoring ceremony. Arrows between two packets are
13
+ network sends; delivery is the ADAPT framework's job — if the receiver is offline, the ADAPT
14
+ broker holds the pending message.
15
+
16
+ **Transaction origins.** Every transaction validates its origin first
17
+ (`current_transaction_info::validate_origin_or_abort`):
18
+
19
+ | Origin | Meaning | Drawn as |
20
+ |--------|---------|----------|
21
+ | `origin::user` | fired by the local host/daemon on behalf of the operator | `Host ->> Packet` |
22
+ | `origin::external` | arrived from the network (envelope carries the authenticated sender in `$from`) | `Packet A ->> Packet B` |
23
+
24
+ **Send mechanisms.** Two kinds of arrows leave a packet:
25
+
26
+ - **Encrypted-channel send** (`encrypted_channel::send_encrypted_tx`) — the normal path between
27
+ registered contacts; requires the peer's address document in `peer_ads`.
28
+ - **Bare send** (`transaction::action::send`) — used only when the peer is *not yet* (or no
29
+ longer) resolvable as a contact: the invite redeem legs and the contact-restore legs. Payload
30
+ confidentiality then comes from a box to an ephemeral key carried in the flow itself, and the
31
+ framework signs every envelope, so the receiver still authenticates the sender.
32
+
33
+ **Results.** A transaction returns *actions*: network sends, `$data` (the caller's return
34
+ value), `$notify_agent` (an event for the host — drawn as a dashed arrow back to the host), and
35
+ `$save_state` (persist the packet — emitted only at the end of a complete step, never
36
+ mid-handshake, so a crash restores to the last stable point).
37
+
38
+ ## The workflows
39
+
40
+ | Workflow | What it covers |
41
+ |----------|----------------|
42
+ | [Contact exchange](./contact-exchange.md) | `generate_invite` → `add_contact` → the three redeem legs |
43
+ | [Send & receive messages](./messaging.md) | `send_message` / `receive_message`, `send_file` / `receive_file`, storage hooks |
44
+ | [Contact restore](./contact-restore.md) | self-healing a degraded contact: restore legs 0–2 + `flush_deferred` |
45
+ | [Monitoring bind & copies](./monitoring.md) | the 6-digit bind ceremony, forced copies, disable |
46
+ | [Control-plane verb calls](./control-verbs.md) | `send_control` → `dispatch` → capability handler → response envelope |
47
+ | [Introductions](./introductions.md) | `core.connect`: a shared CP connects two nodes without an invite |
48
+ | [Cluster lifecycle](./cluster.md) | async child create via host primitives, cluster enrollment, roster push |
49
+ | [Notifications](./notifications.md) | `notify_register` → `notify_issue_tokens` → `send_notification` / `post_notification`, mute/rotate/revoke |
50
+
51
+ All flows were traced from
52
+ [`a2a_messaging.mm`](https://github.com/adapt-toolkit/ours-mufl-core/blob/main/a2a_messaging.mm),
53
+ [`a2a_control.mm`](https://github.com/adapt-toolkit/ours-mufl-core/blob/main/a2a_control.mm),
54
+ [`a2a_capabilities.mm`](https://github.com/adapt-toolkit/ours-mufl-core/blob/main/a2a_capabilities.mm),
55
+ [`a2a_cluster.mm`](https://github.com/adapt-toolkit/ours-mufl-core/blob/main/a2a_cluster.mm) and
56
+ [`a2a_monitoring.mm`](https://github.com/adapt-toolkit/ours-mufl-core/blob/main/a2a_monitoring.mm) and
57
+ [`a2a_notifications.mm`](https://github.com/adapt-toolkit/ours-mufl-core/blob/main/a2a_notifications.mm).