@ouro.bot/friends 0.1.0-alpha.5 → 0.1.0-alpha.7
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/README.md +65 -16
- package/changelog.json +12 -0
- package/dist/a2a-client/index.d.ts +1 -0
- package/dist/a2a-client/index.js +5 -1
- package/dist/a2a-client/roster-verify.d.ts +15 -0
- package/dist/a2a-client/roster-verify.js +61 -0
- package/dist/account-roster.d.ts +52 -0
- package/dist/account-roster.js +108 -0
- package/dist/agent-peer.js +5 -1
- package/dist/audit.d.ts +42 -0
- package/dist/audit.js +86 -0
- package/dist/connect-authority.d.ts +43 -0
- package/dist/connect-authority.js +84 -0
- package/dist/connect.d.ts +55 -0
- package/dist/connect.js +160 -0
- package/dist/coordination.d.ts +17 -1
- package/dist/coordination.js +80 -6
- package/dist/file-bundle.d.ts +5 -0
- package/dist/file-bundle.js +4 -0
- package/dist/friend-lookup.d.ts +9 -0
- package/dist/friend-lookup.js +69 -0
- package/dist/identity.d.ts +17 -0
- package/dist/identity.js +68 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +51 -2
- package/dist/mailbox/index.d.ts +13 -10
- package/dist/mailbox/index.js +1 -1
- package/dist/mcp/dispatch.d.ts +27 -1
- package/dist/mcp/dispatch.js +110 -3
- package/dist/mcp/run-main.js +8 -5
- package/dist/mcp/schemas.js +51 -4
- package/dist/mcp/server.d.ts +9 -0
- package/dist/mcp/server.js +2 -2
- package/dist/mission-result.d.ts +82 -0
- package/dist/mission-result.js +200 -0
- package/dist/mission-store-file.js +8 -0
- package/dist/resolver.d.ts +32 -1
- package/dist/resolver.js +50 -3
- package/dist/roster-store-file.d.ts +16 -0
- package/dist/roster-store-file.js +125 -0
- package/dist/roster-store-memory.d.ts +9 -0
- package/dist/roster-store-memory.js +20 -0
- package/dist/roster-store.d.ts +29 -0
- package/dist/roster-store.js +9 -0
- package/dist/roster-verifier.d.ts +23 -0
- package/dist/roster-verifier.js +47 -0
- package/dist/trust-explanation.d.ts +7 -1
- package/dist/trust-explanation.js +52 -34
- package/dist/trust-mutation.d.ts +13 -1
- package/dist/trust-mutation.js +31 -2
- package/dist/types.d.ts +64 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -39,12 +39,12 @@ narrow:
|
|
|
39
39
|
- **Bring your own storage.** The library never decides *where* or *how* your data lives — you
|
|
40
40
|
pass a path (or a connection string) and, if you want, your own storage backend.
|
|
41
41
|
|
|
42
|
-
It is built as **
|
|
42
|
+
It is built as **six additive capability layers**. Each is a minimal primitive on the one before
|
|
43
43
|
it; none is a workflow engine; removing any layer leaves the ones beneath it unchanged.
|
|
44
44
|
|
|
45
45
|
---
|
|
46
46
|
|
|
47
|
-
## What it does — the
|
|
47
|
+
## What it does — the six capabilities
|
|
48
48
|
|
|
49
49
|
### 1. Identity + the cross-agent moat
|
|
50
50
|
|
|
@@ -158,9 +158,29 @@ an assignee onto anyone (the receiver's own `accept` confirms it — non-transit
|
|
|
158
158
|
advisory metadata rather than a granted capability, and conflicts resolve last-writer-wins by
|
|
159
159
|
timestamp. No queue, no DAG, no workflow DSL.
|
|
160
160
|
|
|
161
|
+
### 6. Own-fleet delegation — link your own agents, then delegate work end-to-end
|
|
162
|
+
|
|
163
|
+
The control-plane thread: the owner can **link two of their own agents** and have one **delegate a
|
|
164
|
+
task** to the other, get it done, and **receive the result back** — over the same consent-gated,
|
|
165
|
+
trust-capped, first-party-inviolable machinery.
|
|
166
|
+
|
|
167
|
+
`connect_to` is a first-class **management-sense** capability — the owner introduces a peer into an
|
|
168
|
+
agent's fleet, but **only from a trusted control surface**: a `local` (owner-only) sense commits
|
|
169
|
+
inline; an `open` sense never does (it downgrades to a confirm-prompt); a `closed` sense is gated by
|
|
170
|
+
a **signed account-roster membership check** (same-account `family` via `same_account`), never a
|
|
171
|
+
blanket allow. The link is recorded as an `action:"connect"` control-plane audit. A bare name with
|
|
172
|
+
no resolvable handle is answered honestly (`needs_handle_or_introduction`) rather than invented.
|
|
173
|
+
|
|
174
|
+
Delegation then rides the layers already here: a **task-spec** travels on a coordination `request`
|
|
175
|
+
(correlated by a minted `requestId`), and the **result-return** (`send_result` / `import_result`)
|
|
176
|
+
carries the actual produced **deliverable** back — attributed to the doer, correlated to the original
|
|
177
|
+
delegation, landing **quarantined** in a separate namespace on import. A result for work you never
|
|
178
|
+
delegated is rejected (`no_delegation`); a stranger's result is refused at the trust cap; first-party
|
|
179
|
+
knowledge is never touched. It is a deliverable channel, **not a remote-exec grant**.
|
|
180
|
+
|
|
161
181
|
> The stack, in one line: agents **recognize** each other (1), **reach** each other (2),
|
|
162
|
-
> **remember** shared work (3), **assess** each other (4),
|
|
163
|
-
> each a minimal primitive on the last.
|
|
182
|
+
> **remember** shared work (3), **assess** each other (4), **negotiate** who does what (5), and the
|
|
183
|
+
> owner **links their own agents to delegate end-to-end** (6) — each a minimal primitive on the last.
|
|
164
184
|
|
|
165
185
|
---
|
|
166
186
|
|
|
@@ -252,7 +272,7 @@ You can also `npm pack` then
|
|
|
252
272
|
Content-Length and newline-delimited JSON — auto-detected from the first message, so it works with
|
|
253
273
|
harnesses on either convention.
|
|
254
274
|
|
|
255
|
-
#### The tool surface —
|
|
275
|
+
#### The tool surface — 32 tools
|
|
256
276
|
|
|
257
277
|
A thin 1:1 mapping over the library (no domain logic in the server):
|
|
258
278
|
|
|
@@ -269,6 +289,7 @@ A thin 1:1 mapping over the library (no domain logic in the server):
|
|
|
269
289
|
| `link_identity` | Link an external identity, merging any orphan record that holds it. |
|
|
270
290
|
| `unlink_identity` | Remove an external identity from a friend. |
|
|
271
291
|
| `onboard_agent` | Upsert an agent-peer record from resolved coordinates (no HTTP fetch). |
|
|
292
|
+
| `connect_to` | **Control plane** — the owner links one of their OWN agents into the fleet (introduce a peer by agentId/did/name at a trust level, default `family`). Authority-gated to a management sense (`local` commits; an `open` sense downgrades to a confirm-prompt; `closed` is gated by a roster/membership check); a bare name with no resolvable handle/DID returns `needs_handle_or_introduction` (never fabricates). Writes an `action:"connect"` control-plane audit. |
|
|
272
293
|
| `whoami` | Resolve the machine owner and which record represents the self. |
|
|
273
294
|
| `channel_caps` | Return a channel's capabilities. |
|
|
274
295
|
| `resolve_room` | Resolve a room (a group's external id) into its members, each with trust context and `knownVia`. |
|
|
@@ -287,13 +308,17 @@ A thin 1:1 mapping over the library (no domain logic in the server):
|
|
|
287
308
|
| `coordinate` | **Producer** — prepare a coordination message (`request` / `offer` / `accept` / `decline` / `handoff`) that negotiates **who** does a mission. |
|
|
288
309
|
| `import_coordination` | **Consumer** — import a coordination message (appends to the mission's coordination log; only a self-`accept` sets the assignee; a `handoff` never forces one). |
|
|
289
310
|
| `get_coordination` | Read a mission's coordination state — its current assignee + the append-only negotiation log. |
|
|
311
|
+
| `send_result` | **Producer** — return B's DELIVERABLE for a delegation, attributed to B + correlated to A's task-spec by `requestId`, named by the mission's `missionKey` (consent-gated via the `coordinate` scope). |
|
|
312
|
+
| `import_result` | **Consumer** — import a result-return; lands B's deliverable quarantined + attributed under `importedResults`, trust-capped; a result whose `requestId` matches no prior first-party delegation is rejected (`no_delegation`); never recomputes status. |
|
|
290
313
|
|
|
291
314
|
The consent tools (`share_profile` / `import_profile` / `grant_share` / `revoke_share` /
|
|
292
|
-
`list_shares`) need a **grant store**; the mission and
|
|
293
|
-
The `friends-mcp` binary wires both
|
|
294
|
-
|
|
295
|
-
`
|
|
296
|
-
|
|
315
|
+
`list_shares`) need a **grant store**; the mission, coordination, and result-return tools
|
|
316
|
+
(`send_result` consumes both) need a **mission store**. The `friends-mcp` binary wires both
|
|
317
|
+
automatically at sibling `_grants/` and `_missions/` directories under `--dir` (plus the
|
|
318
|
+
`_audit/` control-plane log `connect_to` / `set_trust` write through). An embedded server gets
|
|
319
|
+
them by passing `grants` / `missions` / `audit` to `createFriendsMcpServer`. Without the
|
|
320
|
+
relevant store, those tools report `{ ok: false, status: "unsupported" }` and everything else
|
|
321
|
+
works store-only.
|
|
297
322
|
|
|
298
323
|
The server module is consumed in code from the `@ouro.bot/friends/mcp` subpath, exporting
|
|
299
324
|
`createFriendsMcpServer`, `getToolSchemas`, and `runMain`.
|
|
@@ -470,6 +495,10 @@ npm run example:cross-agent-standing # earned standing: first-party-only,
|
|
|
470
495
|
# inert on trust
|
|
471
496
|
npm run example:cross-agent-coordination # the five coordination verbs end-to-end: assignment,
|
|
472
497
|
# non-transitive handoff, last-writer-wins, seeding gate
|
|
498
|
+
npm run example:cross-agent-delegation # own-fleet delegation: two of the owner's agents,
|
|
499
|
+
# same-account family (signed roster), connect_to link,
|
|
500
|
+
# A delegates → B performs → B returns the result → A
|
|
501
|
+
# imports it — every invariant hard-asserted
|
|
473
502
|
```
|
|
474
503
|
|
|
475
504
|
Read them as the honest spec of what the package promises: if a guarantee weren't real, the
|
|
@@ -501,7 +530,7 @@ setNervesEmitter((event) => {
|
|
|
501
530
|
|
|
502
531
|
## Design notes & status
|
|
503
532
|
|
|
504
|
-
- **Store-only, transport-agnostic, additive.** The
|
|
533
|
+
- **Store-only, transport-agnostic, additive.** The six layers were each built as a minimal
|
|
505
534
|
primitive that does not modify the layers beneath it. The cross-agent envelopes are plain data; the
|
|
506
535
|
wire is always the caller's job (the `./mailbox` git-mailbox is one optional, host-driven fallback). A
|
|
507
536
|
CI-enforced dependency rule keeps the core from ever importing the transport.
|
|
@@ -510,8 +539,9 @@ setNervesEmitter((event) => {
|
|
|
510
539
|
clean.
|
|
511
540
|
- **Not a workflow engine.** Each layer deliberately refuses the larger machine it brushes against:
|
|
512
541
|
the mission ledger is not a knowledge base, standing is not a reputation engine, coordination is not
|
|
513
|
-
a scheduler
|
|
514
|
-
|
|
542
|
+
a scheduler, and the delegation channel is a deliverable return — not a remote-exec grant. The
|
|
543
|
+
discipline is the point.
|
|
544
|
+
- **Alpha.** The surface is feature-complete across the six layers but pre-1.0 — expect additive
|
|
515
545
|
changes, and pin a version. Feedback and issues are welcome.
|
|
516
546
|
|
|
517
547
|
### Public API
|
|
@@ -535,7 +565,20 @@ setNervesEmitter((event) => {
|
|
|
535
565
|
`ImportMissionShareResult`, `ImportMissionShareStatus`, `CoordinationEnvelope`,
|
|
536
566
|
`PrepareCoordinationInput`, `PrepareCoordinationResult`, `PrepareCoordinationStatus`,
|
|
537
567
|
`ImportCoordinationInput`, `ImportCoordinationOptions`, `ImportCoordinationResult`,
|
|
538
|
-
`ImportCoordinationStatus
|
|
568
|
+
`ImportCoordinationStatus`, `SetFriendTrustContext`, `AuditSink`, `ControlPlaneAuditRecord`,
|
|
569
|
+
`ResolvedAgentIdentity`, `RosterStore`, `AccountRoster`, `RosterPin`, `RosterVerifier`,
|
|
570
|
+
`AccountMembershipDecision`, `AccountMembershipResult`, `EvaluateAccountMembershipInput`,
|
|
571
|
+
`FriendResolverRosterContext`, `ConnectPeer`, `ConnectAgentsInput`, `ConnectAgentsDeps`,
|
|
572
|
+
`ConnectResult`, `ConnectStatus`, `AuthorizeConnectInput`, `ConnectAuthorization`, `MissionTaskSpec`,
|
|
573
|
+
`MissionResult`, `MissionResultEnvelope`, `PrepareMissionResultInput`, `PrepareMissionResultResult`,
|
|
574
|
+
`PrepareMissionResultStatus`, `ImportMissionResultInput`, `ImportMissionResultOptions`,
|
|
575
|
+
`ImportMissionResultResult`, `ImportMissionResultStatus`. (`TrustBasis` additively gains the
|
|
576
|
+
`"same_account"` member — the basis for family granted via the signed account roster — and `AgentMeta`
|
|
577
|
+
additively gains an optional `identity { did, pinnedKey?, handle?, pinnedAt? }` durable-identity home;
|
|
578
|
+
both are schemaVersion-1 additive, and a legacy `a2a.did` migrates-on-read into `identity.did`.
|
|
579
|
+
`MissionRecord` additively gains the own-fleet delegation namespaces `delegations` / `importedDelegations`
|
|
580
|
+
(gap-1) and `results` / `importedResults` (gap-2), and `ControlPlaneAuditRecord.action` widens additively
|
|
581
|
+
to `"set_trust" | "connect"`.)
|
|
539
582
|
|
|
540
583
|
**Values:** `TRUSTED_LEVELS`, `IDENTITY_SCOPES`, `isTrustedLevel`, `isIdentityProvider`,
|
|
541
584
|
`isIntegration`, `isShareScope`, `isCoordinationIntent`, `FileFriendStore`, `FileGrantStore`,
|
|
@@ -548,7 +591,11 @@ setNervesEmitter((event) => {
|
|
|
548
591
|
`trustImpliedPolicy`, `tieredPolicy`, `DEFAULT_CONSENT_POLICY`, `tofuVerifier`,
|
|
549
592
|
`DEFAULT_AGENT_VERIFIER`, `prepareProfileShare`, `importProfileShare`, `prepareMissionShare`,
|
|
550
593
|
`importMissionShare`, `prepareCoordination`, `importCoordination`, `grantShare`, `revokeShare`,
|
|
551
|
-
`listShares`, `isGrantEffective`, `setNervesEmitter`, `emitNervesEvent
|
|
594
|
+
`listShares`, `isGrantEffective`, `setNervesEmitter`, `emitNervesEvent`, `resolveAgentIdentity`,
|
|
595
|
+
`withMigratedIdentity`, `findFriendByDid`, `MemoryAuditSink`, `FileAuditSink`, `auditPathFor`,
|
|
596
|
+
`FileRosterStore`, `rostersDirFor`, `MemoryRosterStore`, `identityRosterVerifier`,
|
|
597
|
+
`DEFAULT_ROSTER_VERIFIER`, `evaluateAccountMembership`, `connectAgents`, `authorizeConnect`,
|
|
598
|
+
`prepareMissionResult`, `importMissionResult`.
|
|
552
599
|
|
|
553
600
|
**From `@ouro.bot/friends/mcp`:** `createFriendsMcpServer`, `getToolSchemas`, `runMain` (plus the
|
|
554
601
|
`McpToolSchema`, `FriendsMcpServer`, and `RunMainIo` types).
|
|
@@ -565,7 +612,9 @@ setNervesEmitter((event) => {
|
|
|
565
612
|
identity helpers `parseDidKey` / `keyAgreementFromDidKey` / `didKeyIdentityFromEd25519` /
|
|
566
613
|
`ed25519PubToDidKey` and `didWebToUrl` / `resolveDidWeb` / `parseDidDocument`; the primitives
|
|
567
614
|
`sealTo` / `openSealed`, `signEnvelope` / `verifyEnvelopeSignature`, `jcsString` / `jcsBytes`, and
|
|
568
|
-
the `ready` init seam
|
|
615
|
+
the `ready` init seam; and the account-roster Ed25519 verify `ed25519RosterVerifier` / `signRoster`
|
|
616
|
+
(the crypto implementation of the core `RosterVerifier` seam — host-injected, so the core stays
|
|
617
|
+
transport-free) (plus the `A2ATransport`, `DidResolution`, `SealedEnvelope`, `StructuredProof`,
|
|
569
618
|
`ReachabilityPlan`, `FriendsAgentCard`, `DidKeyIdentity`, `DidDocument` types). The transports
|
|
570
619
|
(direct A2A / relay / git op) are injected by the host — this module does no network or git itself.
|
|
571
620
|
|
package/changelog.json
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"versions": [
|
|
3
|
+
{
|
|
4
|
+
"version": "0.1.0-alpha.7",
|
|
5
|
+
"changes": [
|
|
6
|
+
"p11 increment 2 — own-fleet delegation: connect_to control plane + the LOCAL delegate->perform->return proof (task-spec + result-return). Brick 8 (greenfield): connectAgents + a NEW management-sense authority predicate (authorizeConnect) — the owner links one of their own agents into the fleet, gated to a management sense (local commits inline; an open sense downgrades to a confirm-prompt; closed is gated by a signed account-roster membership check, NEVER a blanket allow), with disambiguation honesty (a bare name with no resolvable handle/DID returns needs_handle_or_introduction, never fabricated). connect_to is a library fn + an MCP tool, writes an action:\"connect\" control-plane audit (ControlPlaneAuditRecord.action widens additively to \"set_trust\" | \"connect\"), and the MCP boundary maps the stdio origin to a local management senseType (a network transport passes its real senseType + a pre-computed roster membership). gap-1 (the task-spec): CoordinationEnvelope gains an optional task? (MissionTaskSpec with a minted requestId) on the delegation request — recorded first-party under MissionRecord.delegations[requestId], imported quarantined under importedDelegations[agentId][requestId] (trust-capped, non-transitive, first-party-inviolable). gap-2 (the result-return): a NEW prepareMissionResult/importMissionResult envelope (twin of mission-share, NOT a reuse) carries B's actual DELIVERABLE back to A — attributed to B, correlated by missionKey + requestId, consent-gated via the existing \"coordinate\" scope (no new ShareScope); on import it lands quarantined + attributed under importedResults, rejects a result whose requestId matches no prior first-party delegation (no_delegation — correlation honesty), rejects a result whose source is not the agent the work was delegated TO (assignee_mismatch — assignee honesty; the delegation now persists its assignee and the import enforces source === assignee, failing closed on a legacy assignee-less record), never seeds a mission (no_mission), is trust-capped (untrusted_source before correlation), non-transitive, idempotent on replay. The mailbox kind union widens additively to carry kind:\"mission_result\". 3 new MCP tools (connect_to, send_result, import_result) — 29 -> 32; the coordinate tool now threads the task-spec. FileMissionStore.normalize round-trips the four new additive MissionRecord namespaces (delegations/importedDelegations/results/importedResults). A LOCAL hermetic proof (examples/cross-agent-delegation.ts): two own-fleet agents on separate stores, recognized as same-account family via a signed roster (real ed25519) on both sides, linked by the owner via connect_to, then A delegates a task -> B performs it -> B returns the deliverable -> A imports it — every invariant hard-asserted (missionKey-not-UUID, first-party-inviolable, non-transitive, trust cap, orphan-reject, trusted-non-assignee-reject, replay-inert, no third-party leak); exit 0, with a negative control that bites. Security review (inc-2): finding 1 (MEDIUM, cross-delegation result injection) FIXED as above — a trusted peer that is not the assignee can no longer inject a forged result for a requestId delegated to a different agent; findings 2-3 (LOW, dormant networking pre-conditions) DOCUMENTED at the connect_to authority gate + introduce-default site (before any non-local/networked controlContext is wired, the connect commit must add target-side roster verification and validate the caller-supplied trustLevel against the authority decision); findings 4-5 (existence oracle, vestigial envelope.fromAgentId) accepted by-design (finding 5 noted at its site). Core stays transport-free (the core⊥a2a-client + mailbox⊥mcp CI rules unmodified); 100% coverage maintained."
|
|
7
|
+
]
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"version": "0.1.0-alpha.6",
|
|
11
|
+
"changes": [
|
|
12
|
+
"p11 increment 1 — own-fleet foundation. Bug A: cold A2A contact defaults to stranger (safe-by-default; explicit owner-initiated trustLevel still wins). Bug B: append-only control-plane audit (AuditSink + MemoryAuditSink/FileAuditSink) on every setFriendTrust, now wired into the live MCP dispatch path (set_trust + the onboard_agent trust seat) via openFileBundle's FileAuditSink, attributed to the owner-only stdio boundary. Bug C: FriendResolver is roster-aware — a key-verified member of the pinned account roster is recognized as family across OS users, without loosening the cold-A2A stranger default. DID re-key (additive, schemaVersion still 1): AgentMeta.identity durable home with a2a.did migrate-on-read; did-aware findFriendByDid. Account roster: RosterStore/FileRosterStore/MemoryRosterStore + the RosterVerifier seam (core identity-only default; a2a-client ed25519RosterVerifier Ed25519 impl) granting family via a new same_account TrustBasis only to a TOFU-pinned, key-verified roster member (changed roster key hard-fails). Security hardening of the trust primitives: the family-grant path FAILS CLOSED — the identity-only default can never grant family (only a cryptographic verifier can, gated by a grantsFamily capability), with a loud one-time warning; evaluateAccountMembership requires an unforgeable VerifiedCandidate so the did-control precondition can't be skipped; findFriendByDid rejects falsy-did queries and no longer rewards back-dating on a duplicate-did anomaly (prefers the pinned record, warns); empty-string DIDs are never matchable keys; and FileRosterStore guards accountId against path traversal. Core remains transport-free (the CI core⊥a2a-client dependency rule is unmodified)."
|
|
13
|
+
]
|
|
14
|
+
},
|
|
3
15
|
{
|
|
4
16
|
"version": "0.1.0-alpha.5",
|
|
5
17
|
"changes": [
|
|
@@ -19,5 +19,6 @@ export { buildFriendsAgentCard } from "./agent-card";
|
|
|
19
19
|
export type { A2ACapabilities, A2ASkill, FriendsAgentCard } from "./agent-card";
|
|
20
20
|
export { resolveReachability } from "./reachability";
|
|
21
21
|
export type { ReachabilityPlan } from "./reachability";
|
|
22
|
+
export { ed25519RosterVerifier, signRoster } from "./roster-verify";
|
|
22
23
|
export { receiveShare, sendShare } from "./adapter";
|
|
23
24
|
export type { A2ATransport, DidResolution, ReceiveShareInput, ReceiveShareResult, SeenLedgerLike, SendShareInput, SendShareResult, } from "./adapter";
|
package/dist/a2a-client/index.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// recipient DID bound into the AEAD AD), so a relay carries CIPHERTEXT ONLY — it
|
|
11
11
|
// can never read, forge, tamper, re-target, replay-to-effect, or escalate.
|
|
12
12
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
-
exports.sendShare = exports.receiveShare = exports.resolveReachability = exports.buildFriendsAgentCard = exports.wrapInDataPart = exports.unwrapDataPart = exports.sealEnvelope = exports.openSealedEnvelope = exports.verifyCardDidBinding = exports.signSuccessor = exports.pinOnFirstContact = exports.MemoryPinStore = exports.isPinned = exports.getPinned = exports.evaluateRotation = exports.DidVerifier = exports.resolveDidWeb = exports.parseDidDocument = exports.didWebToUrl = exports.parseDidKey = exports.keyAgreementFromDidKey = exports.ed25519PubToDidKey = exports.didKeyIdentityFromEd25519 = exports.base58btcEncode = exports.base58btcDecode = exports.verifyEnvelopeSignature = exports.signEnvelope = exports.serializeProof = exports.parseProof = exports.SealOpenError = exports.sealTo = exports.openSealed = exports.jcsString = exports.jcsBytes = exports.ready = void 0;
|
|
13
|
+
exports.sendShare = exports.receiveShare = exports.signRoster = exports.ed25519RosterVerifier = exports.resolveReachability = exports.buildFriendsAgentCard = exports.wrapInDataPart = exports.unwrapDataPart = exports.sealEnvelope = exports.openSealedEnvelope = exports.verifyCardDidBinding = exports.signSuccessor = exports.pinOnFirstContact = exports.MemoryPinStore = exports.isPinned = exports.getPinned = exports.evaluateRotation = exports.DidVerifier = exports.resolveDidWeb = exports.parseDidDocument = exports.didWebToUrl = exports.parseDidKey = exports.keyAgreementFromDidKey = exports.ed25519PubToDidKey = exports.didKeyIdentityFromEd25519 = exports.base58btcEncode = exports.base58btcDecode = exports.verifyEnvelopeSignature = exports.signEnvelope = exports.serializeProof = exports.parseProof = exports.SealOpenError = exports.sealTo = exports.openSealed = exports.jcsString = exports.jcsBytes = exports.ready = void 0;
|
|
14
14
|
// ── init seam ──
|
|
15
15
|
var sodium_1 = require("./sodium");
|
|
16
16
|
Object.defineProperty(exports, "ready", { enumerable: true, get: function () { return sodium_1.ready; } });
|
|
@@ -66,6 +66,10 @@ Object.defineProperty(exports, "buildFriendsAgentCard", { enumerable: true, get:
|
|
|
66
66
|
// ── reachability ladder ──
|
|
67
67
|
var reachability_1 = require("./reachability");
|
|
68
68
|
Object.defineProperty(exports, "resolveReachability", { enumerable: true, get: function () { return reachability_1.resolveReachability; } });
|
|
69
|
+
// ── account-roster Ed25519 verify (the RosterVerifier seam's crypto impl) ──
|
|
70
|
+
var roster_verify_1 = require("./roster-verify");
|
|
71
|
+
Object.defineProperty(exports, "ed25519RosterVerifier", { enumerable: true, get: function () { return roster_verify_1.ed25519RosterVerifier; } });
|
|
72
|
+
Object.defineProperty(exports, "signRoster", { enumerable: true, get: function () { return roster_verify_1.signRoster; } });
|
|
69
73
|
// ── send / receive adapter ──
|
|
70
74
|
var adapter_1 = require("./adapter");
|
|
71
75
|
Object.defineProperty(exports, "receiveShare", { enumerable: true, get: function () { return adapter_1.receiveShare; } });
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Sodium } from "./sodium";
|
|
2
|
+
import type { RosterVerifier } from "../roster-verifier";
|
|
3
|
+
import type { AccountRoster } from "../roster-store";
|
|
4
|
+
/** A RosterVerifier that checks the roster's Ed25519 signature against the base64
|
|
5
|
+
* `rosterKey`. Returns false (never throws) on a malformed key/sig or a bad
|
|
6
|
+
* signature. (Unit 7a stub — verify not implemented.) */
|
|
7
|
+
export declare function ed25519RosterVerifier(sodium: Sodium): RosterVerifier;
|
|
8
|
+
/** Test/host helper: produce a valid roster `sig` by signing `rosterSigningBytes`
|
|
9
|
+
* with the account's Ed25519 private key (mirrors `signSuccessor`/`signEnvelope`).
|
|
10
|
+
* Returns the base64 (ORIGINAL) detached signature. */
|
|
11
|
+
export declare function signRoster(input: {
|
|
12
|
+
sodium: Sodium;
|
|
13
|
+
accountKeyPriv: Uint8Array;
|
|
14
|
+
roster: Omit<AccountRoster, "sig">;
|
|
15
|
+
}): string;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ed25519RosterVerifier = ed25519RosterVerifier;
|
|
4
|
+
exports.signRoster = signRoster;
|
|
5
|
+
// ed25519RosterVerifier — the RosterVerifier seam's REAL crypto implementation
|
|
6
|
+
// (a2a-client side; MAY import libsodium/jcs/sign). Mirrors how DidVerifier
|
|
7
|
+
// implements the AgentVerifier seam. The host injects this so the core stays
|
|
8
|
+
// transport-free.
|
|
9
|
+
//
|
|
10
|
+
// Contract (shared with src/roster-verifier.ts): the roster `sig` is an Ed25519
|
|
11
|
+
// detached signature over `jcsBytes({ accountId, members, epoch })` — the roster
|
|
12
|
+
// MINUS `sig` — exactly how `verifyEnvelopeSignature` signs the proof-stripped
|
|
13
|
+
// envelope. `rosterKey` is the base64 (ORIGINAL) Ed25519 public key.
|
|
14
|
+
const jcs_1 = require("./jcs");
|
|
15
|
+
/** The canonical bytes the roster `sig` is computed over: the roster minus its
|
|
16
|
+
* `sig` field, JCS-canonicalized. Both signer and verifier MUST produce these
|
|
17
|
+
* identical bytes. */
|
|
18
|
+
function rosterSigningBytes(roster) {
|
|
19
|
+
return (0, jcs_1.jcsBytes)({ accountId: roster.accountId, members: roster.members, epoch: roster.epoch });
|
|
20
|
+
}
|
|
21
|
+
/** A RosterVerifier that checks the roster's Ed25519 signature against the base64
|
|
22
|
+
* `rosterKey`. Returns false (never throws) on a malformed key/sig or a bad
|
|
23
|
+
* signature. (Unit 7a stub — verify not implemented.) */
|
|
24
|
+
function ed25519RosterVerifier(sodium) {
|
|
25
|
+
return {
|
|
26
|
+
// SECURITY (finding 1): the REAL cryptographic verifier — the only RosterVerifier
|
|
27
|
+
// strong enough to back a family grant. `evaluateAccountMembership` checks this
|
|
28
|
+
// flag and fails closed (→ unverified) under any verifier that lacks it.
|
|
29
|
+
grantsFamily: true,
|
|
30
|
+
verify(roster, rosterKey) {
|
|
31
|
+
let pub;
|
|
32
|
+
let sig;
|
|
33
|
+
try {
|
|
34
|
+
pub = sodium.from_base64(rosterKey, sodium.base64_variants.ORIGINAL);
|
|
35
|
+
sig = sodium.from_base64(roster.sig, sodium.base64_variants.ORIGINAL);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
// Malformed base64 in the key or sig → a failed verification, never a throw.
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
const msg = rosterSigningBytes(roster);
|
|
42
|
+
try {
|
|
43
|
+
return sodium.crypto_sign_verify_detached(sig, msg, pub);
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
// A wrong-length key/sig can throw inside libsodium — treat as a failed
|
|
47
|
+
// verification, never an uncaught error (mirrors verifyEnvelopeSignature).
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/** Test/host helper: produce a valid roster `sig` by signing `rosterSigningBytes`
|
|
54
|
+
* with the account's Ed25519 private key (mirrors `signSuccessor`/`signEnvelope`).
|
|
55
|
+
* Returns the base64 (ORIGINAL) detached signature. */
|
|
56
|
+
function signRoster(input) {
|
|
57
|
+
const { sodium, accountKeyPriv, roster } = input;
|
|
58
|
+
const msg = (0, jcs_1.jcsBytes)({ accountId: roster.accountId, members: roster.members, epoch: roster.epoch });
|
|
59
|
+
const sig = sodium.crypto_sign_detached(msg, accountKeyPriv);
|
|
60
|
+
return sodium.to_base64(sig, sodium.base64_variants.ORIGINAL);
|
|
61
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { AccountRoster, RosterStore } from "./roster-store";
|
|
2
|
+
import type { RosterVerifier } from "./roster-verifier";
|
|
3
|
+
export type AccountMembershipDecision = "family_same_account" | "not_member" | "unverified" | "roster_key_mismatch";
|
|
4
|
+
/** SECURITY (finding 2, HIGH): an opaque "the caller has verified this peer controls
|
|
5
|
+
* this did" token. `evaluateAccountMembership` grants family ONLY for a value of this
|
|
6
|
+
* type, and the ONLY way to produce one is `verifiedCandidate(did)` — so the
|
|
7
|
+
* candidate-DID precondition is impossible to forget at a call site (a bare string
|
|
8
|
+
* does not type-check). The private brand makes it unforgeable from a plain object. */
|
|
9
|
+
export interface VerifiedCandidate {
|
|
10
|
+
readonly did: string;
|
|
11
|
+
/** Private brand — prevents `{ did }` from structurally satisfying the type. */
|
|
12
|
+
readonly [VERIFIED_BRAND]: true;
|
|
13
|
+
}
|
|
14
|
+
declare const VERIFIED_BRAND: unique symbol;
|
|
15
|
+
/** Mint a {@link VerifiedCandidate}. CALLING THIS IS AN ASSERTION: the caller has
|
|
16
|
+
* already proven (via a DID/pinned-key handshake — e.g. the a2a-client sealed-envelope
|
|
17
|
+
* gate that runs `DidVerifier` before this) that the peer controls `did`. Never call
|
|
18
|
+
* it on an attacker-supplied did that has not been authenticated. */
|
|
19
|
+
export declare function verifiedCandidate(did: string): VerifiedCandidate;
|
|
20
|
+
export interface EvaluateAccountMembershipInput {
|
|
21
|
+
roster: AccountRoster;
|
|
22
|
+
/** SECURITY (finding 2): the verified candidate — only mintable via
|
|
23
|
+
* `verifiedCandidate(did)` after the caller has authenticated the peer's control of
|
|
24
|
+
* the did. The roster membership + sig checks are NOT a proof of did-control on
|
|
25
|
+
* their own; this token supplies that missing precondition. */
|
|
26
|
+
candidate: VerifiedCandidate;
|
|
27
|
+
rosterKey: string;
|
|
28
|
+
store: RosterStore;
|
|
29
|
+
verifier?: RosterVerifier;
|
|
30
|
+
}
|
|
31
|
+
export interface AccountMembershipResult {
|
|
32
|
+
decision: AccountMembershipDecision;
|
|
33
|
+
reason?: string;
|
|
34
|
+
}
|
|
35
|
+
/** Test seam: reset the one-time-warning latch so a test can assert the loud warning
|
|
36
|
+
* fires (and de-dupes) deterministically, independent of test order. */
|
|
37
|
+
export declare function _resetRosterVerifierWarningForTest(): void;
|
|
38
|
+
/** Decide whether the VERIFIED `candidate` is family-via-same-account under `roster`.
|
|
39
|
+
*
|
|
40
|
+
* Preconditions (all enforced, not merely documented):
|
|
41
|
+
* - The caller has authenticated that the peer controls `candidate.did` (carried by
|
|
42
|
+
* the unforgeable {@link VerifiedCandidate} — finding 2). Membership + sig are NOT
|
|
43
|
+
* a substitute for did-control.
|
|
44
|
+
* - A real cryptographic `verifier` (`grantsFamily: true`) is injected. The
|
|
45
|
+
* identity-only default fails closed: it can verify identity for non-grant checks
|
|
46
|
+
* but can NEVER produce a `family_same_account` grant (finding 1).
|
|
47
|
+
*
|
|
48
|
+
* Flow: TOFU-pin the roster key on first contact; a changed key hard-fails; the
|
|
49
|
+
* verifier must accept the roster; the verifier must be family-granting; the
|
|
50
|
+
* candidate's did must be in the roster. Any miss yields a non-family decision. */
|
|
51
|
+
export declare function evaluateAccountMembership(input: EvaluateAccountMembershipInput): Promise<AccountMembershipResult>;
|
|
52
|
+
export {};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.verifiedCandidate = verifiedCandidate;
|
|
4
|
+
exports._resetRosterVerifierWarningForTest = _resetRosterVerifierWarningForTest;
|
|
5
|
+
exports.evaluateAccountMembership = evaluateAccountMembership;
|
|
6
|
+
// evaluateAccountMembership — the Increment-1 payoff (Item 3). Grants `family` via
|
|
7
|
+
// TrustBasis "same_account" ONLY to a peer whose `did` is in the pinned roster AND
|
|
8
|
+
// whose roster verifies under the TOFU-pinned roster key. A changed roster key
|
|
9
|
+
// HARD-FAILS (no silent re-pin).
|
|
10
|
+
//
|
|
11
|
+
// CORE module: it uses the INJECTED RosterVerifier + RosterStore seams and does NO
|
|
12
|
+
// direct crypto (no a2a-client / libsodium import) — the Ed25519 verifier is
|
|
13
|
+
// injected by the host/test. The lint enforces the dependency direction.
|
|
14
|
+
const observability_1 = require("./observability");
|
|
15
|
+
const roster_verifier_1 = require("./roster-verifier");
|
|
16
|
+
/** Mint a {@link VerifiedCandidate}. CALLING THIS IS AN ASSERTION: the caller has
|
|
17
|
+
* already proven (via a DID/pinned-key handshake — e.g. the a2a-client sealed-envelope
|
|
18
|
+
* gate that runs `DidVerifier` before this) that the peer controls `did`. Never call
|
|
19
|
+
* it on an attacker-supplied did that has not been authenticated. */
|
|
20
|
+
function verifiedCandidate(did) {
|
|
21
|
+
return { did };
|
|
22
|
+
}
|
|
23
|
+
/** One-time loud-warning latch: we warn at most once per process when a family grant
|
|
24
|
+
* is refused purely because the active verifier is not cryptographic (finding 1). */
|
|
25
|
+
let warnedNonCryptographicVerifier = false;
|
|
26
|
+
/** Test seam: reset the one-time-warning latch so a test can assert the loud warning
|
|
27
|
+
* fires (and de-dupes) deterministically, independent of test order. */
|
|
28
|
+
function _resetRosterVerifierWarningForTest() {
|
|
29
|
+
warnedNonCryptographicVerifier = false;
|
|
30
|
+
}
|
|
31
|
+
/** Decide whether the VERIFIED `candidate` is family-via-same-account under `roster`.
|
|
32
|
+
*
|
|
33
|
+
* Preconditions (all enforced, not merely documented):
|
|
34
|
+
* - The caller has authenticated that the peer controls `candidate.did` (carried by
|
|
35
|
+
* the unforgeable {@link VerifiedCandidate} — finding 2). Membership + sig are NOT
|
|
36
|
+
* a substitute for did-control.
|
|
37
|
+
* - A real cryptographic `verifier` (`grantsFamily: true`) is injected. The
|
|
38
|
+
* identity-only default fails closed: it can verify identity for non-grant checks
|
|
39
|
+
* but can NEVER produce a `family_same_account` grant (finding 1).
|
|
40
|
+
*
|
|
41
|
+
* Flow: TOFU-pin the roster key on first contact; a changed key hard-fails; the
|
|
42
|
+
* verifier must accept the roster; the verifier must be family-granting; the
|
|
43
|
+
* candidate's did must be in the roster. Any miss yields a non-family decision. */
|
|
44
|
+
async function evaluateAccountMembership(input) {
|
|
45
|
+
const { roster, candidate, rosterKey, store } = input;
|
|
46
|
+
const accountId = roster.accountId;
|
|
47
|
+
// 1) Roster-key pin (TOFU). First contact pins the key; an EXISTING pin for a
|
|
48
|
+
// DIFFERENT key HARD-FAILS (no silent re-pin); a matching pin proceeds.
|
|
49
|
+
const existingPin = await store.getPin(accountId);
|
|
50
|
+
if (!existingPin) {
|
|
51
|
+
await store.putPin({ accountId, rosterKey, pinnedAt: new Date().toISOString() });
|
|
52
|
+
}
|
|
53
|
+
else if (existingPin.rosterKey !== rosterKey) {
|
|
54
|
+
const result = {
|
|
55
|
+
decision: "roster_key_mismatch",
|
|
56
|
+
reason: "presented roster key does not match the pinned key",
|
|
57
|
+
};
|
|
58
|
+
emit(result.decision, accountId);
|
|
59
|
+
return result;
|
|
60
|
+
}
|
|
61
|
+
// 2) Authenticity: the injected verifier (or the identity default) must accept
|
|
62
|
+
// the roster under the pinned/presented key.
|
|
63
|
+
const verifier = input.verifier ?? roster_verifier_1.DEFAULT_ROSTER_VERIFIER;
|
|
64
|
+
if (!verifier.verify(roster, rosterKey)) {
|
|
65
|
+
const result = { decision: "unverified", reason: "roster signature did not verify" };
|
|
66
|
+
emit(result.decision, accountId);
|
|
67
|
+
return result;
|
|
68
|
+
}
|
|
69
|
+
// 2b) SECURITY (finding 1, HIGH): FAIL CLOSED on the family-granting path. The
|
|
70
|
+
// identity-only default accepts any well-formed roster (it ignores the sig), so it
|
|
71
|
+
// MUST NOT be allowed to grant family — only a real cryptographic verifier
|
|
72
|
+
// (`grantsFamily: true`) can. Without one, the strongest tier is unreachable: a
|
|
73
|
+
// would-be member is `unverified`, never `family_same_account`. Warn LOUDLY once.
|
|
74
|
+
if (verifier.grantsFamily !== true) {
|
|
75
|
+
if (!warnedNonCryptographicVerifier) {
|
|
76
|
+
warnedNonCryptographicVerifier = true;
|
|
77
|
+
(0, observability_1.emitNervesEvent)({
|
|
78
|
+
level: "warn",
|
|
79
|
+
component: "friends",
|
|
80
|
+
event: "friends.roster_verifier_not_cryptographic",
|
|
81
|
+
message: "REFUSING to grant family_same_account: no cryptographic RosterVerifier injected (the identity-only default cannot back a family grant). Inject ed25519RosterVerifier to enable same-account family.",
|
|
82
|
+
meta: { accountId },
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
const result = {
|
|
86
|
+
decision: "unverified",
|
|
87
|
+
reason: "no cryptographic roster verifier injected — family grant withheld (fail-closed)",
|
|
88
|
+
};
|
|
89
|
+
emit(result.decision, accountId);
|
|
90
|
+
return result;
|
|
91
|
+
}
|
|
92
|
+
// 3) Membership: the candidate's did must be in the verified roster.
|
|
93
|
+
const isMember = roster.members.some((m) => m.did === candidate.did);
|
|
94
|
+
const result = isMember
|
|
95
|
+
? { decision: "family_same_account" }
|
|
96
|
+
: { decision: "not_member", reason: "candidate did is not in the roster" };
|
|
97
|
+
emit(result.decision, accountId);
|
|
98
|
+
return result;
|
|
99
|
+
}
|
|
100
|
+
/** Emit the membership-evaluated nerves event. */
|
|
101
|
+
function emit(decision, accountId) {
|
|
102
|
+
(0, observability_1.emitNervesEvent)({
|
|
103
|
+
component: "friends",
|
|
104
|
+
event: "friends.account_membership_evaluated",
|
|
105
|
+
message: "evaluated account-roster membership",
|
|
106
|
+
meta: { accountId, decision },
|
|
107
|
+
});
|
|
108
|
+
}
|
package/dist/agent-peer.js
CHANGED
|
@@ -13,7 +13,11 @@ async function upsertAgentPeer(store, input) {
|
|
|
13
13
|
const { name, agentId, a2a, bundleName } = input;
|
|
14
14
|
const existing = await store.findByExternalId("a2a-agent", agentId);
|
|
15
15
|
const now = new Date().toISOString();
|
|
16
|
-
|
|
16
|
+
// Bug A — cold contact is safe-by-default: a brand-new peer with no explicit
|
|
17
|
+
// trustLevel and no existing record lands at `stranger`, not `acquaintance`. An
|
|
18
|
+
// owner-initiated onboard that passes an explicit `trustLevel`, and an existing
|
|
19
|
+
// record's level, both still win (they precede this fallback).
|
|
20
|
+
const trustLevel = input.trustLevel ?? existing?.trustLevel ?? "stranger";
|
|
17
21
|
const baseMeta = existing?.agentMeta ?? {
|
|
18
22
|
bundleName: bundleName ?? name,
|
|
19
23
|
familiarity: 0,
|
package/dist/audit.d.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { TrustBasis } from "./trust-explanation";
|
|
2
|
+
import type { TrustLevel } from "./types";
|
|
3
|
+
/** One append-only control-plane audit record. Captures a single control-plane
|
|
4
|
+
* mutation: WHO (`actor`), to WHOM (`targetId` / optional `targetDid`), the resulting
|
|
5
|
+
* `level`, the `basis` it was granted on, the `originSense` it came through, and WHEN
|
|
6
|
+
* (`ts`). The `action` discriminates the mutation kind: `"set_trust"` (a trust-level
|
|
7
|
+
* change — the `setFriendTrust` mutation + the `onboard_agent` trust seat) or
|
|
8
|
+
* `"connect"` (an owner linking one of their own agents into the fleet via `connect_to`
|
|
9
|
+
* — p11 inc2; additive, the JSONL append is value-agnostic so only the type widened). */
|
|
10
|
+
export interface ControlPlaneAuditRecord {
|
|
11
|
+
action: "set_trust" | "connect";
|
|
12
|
+
targetId: string;
|
|
13
|
+
targetDid?: string;
|
|
14
|
+
level: TrustLevel;
|
|
15
|
+
basis?: TrustBasis;
|
|
16
|
+
actor: string;
|
|
17
|
+
originSense?: string;
|
|
18
|
+
ts: string;
|
|
19
|
+
}
|
|
20
|
+
/** The append-only sink a control-plane mutation writes through. The host
|
|
21
|
+
* implements it (in-memory in tests, a file/JSONL adapter in production). */
|
|
22
|
+
export interface AuditSink {
|
|
23
|
+
append(record: ControlPlaneAuditRecord): Promise<void> | void;
|
|
24
|
+
}
|
|
25
|
+
/** In-memory append-only sink — test/host convenience, mirroring MemoryPinStore.
|
|
26
|
+
* `list()` exposes the records in append order; there is no overwrite. */
|
|
27
|
+
export declare class MemoryAuditSink implements AuditSink {
|
|
28
|
+
private readonly records;
|
|
29
|
+
append(record: ControlPlaneAuditRecord): void;
|
|
30
|
+
list(): ControlPlaneAuditRecord[];
|
|
31
|
+
}
|
|
32
|
+
/** The append-only control-plane log file for a given friends directory:
|
|
33
|
+
* `<friendsDir>/_audit/control.jsonl`. A reserved `_`-prefixed sibling (like
|
|
34
|
+
* `_grants/`) so one `--dir` covers it; JSONL so appends never rewrite history. */
|
|
35
|
+
export declare function auditPathFor(friendsDir: string): string;
|
|
36
|
+
/** Filesystem AuditSink — appends each record as one JSON line to
|
|
37
|
+
* `_audit/control.jsonl`. mkdir-on-construct, mirroring FileGrantStore. */
|
|
38
|
+
export declare class FileAuditSink implements AuditSink {
|
|
39
|
+
private readonly filePath;
|
|
40
|
+
constructor(filePath: string);
|
|
41
|
+
append(record: ControlPlaneAuditRecord): Promise<void>;
|
|
42
|
+
}
|
package/dist/audit.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.FileAuditSink = exports.MemoryAuditSink = void 0;
|
|
37
|
+
exports.auditPathFor = auditPathFor;
|
|
38
|
+
// Control-plane audit (Bug B) — an append-only record of every trust mutation.
|
|
39
|
+
//
|
|
40
|
+
// The control plane is "who changed a peer's standing, from where, and why". The
|
|
41
|
+
// package must stay storage-agnostic (and 100%-coverable), so the audit is an
|
|
42
|
+
// injectable SINK — not a hard-wired `fs` write — mirroring the observability
|
|
43
|
+
// seam and the GrantStore/FileGrantStore split. `setFriendTrust` writes one record
|
|
44
|
+
// on a successful mutation; the host wires a `FileAuditSink` (or its own) to
|
|
45
|
+
// persist it. With no sink injected, the mutation is unchanged (no-op audit).
|
|
46
|
+
const fs = __importStar(require("fs"));
|
|
47
|
+
const fsPromises = __importStar(require("fs/promises"));
|
|
48
|
+
const path = __importStar(require("path"));
|
|
49
|
+
const observability_1 = require("./observability");
|
|
50
|
+
/** In-memory append-only sink — test/host convenience, mirroring MemoryPinStore.
|
|
51
|
+
* `list()` exposes the records in append order; there is no overwrite. */
|
|
52
|
+
class MemoryAuditSink {
|
|
53
|
+
records = [];
|
|
54
|
+
append(record) {
|
|
55
|
+
this.records.push(record);
|
|
56
|
+
}
|
|
57
|
+
list() {
|
|
58
|
+
return [...this.records];
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
exports.MemoryAuditSink = MemoryAuditSink;
|
|
62
|
+
/** The append-only control-plane log file for a given friends directory:
|
|
63
|
+
* `<friendsDir>/_audit/control.jsonl`. A reserved `_`-prefixed sibling (like
|
|
64
|
+
* `_grants/`) so one `--dir` covers it; JSONL so appends never rewrite history. */
|
|
65
|
+
function auditPathFor(friendsDir) {
|
|
66
|
+
return path.join(friendsDir, "_audit", "control.jsonl");
|
|
67
|
+
}
|
|
68
|
+
/** Filesystem AuditSink — appends each record as one JSON line to
|
|
69
|
+
* `_audit/control.jsonl`. mkdir-on-construct, mirroring FileGrantStore. */
|
|
70
|
+
class FileAuditSink {
|
|
71
|
+
filePath;
|
|
72
|
+
constructor(filePath) {
|
|
73
|
+
this.filePath = filePath;
|
|
74
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
75
|
+
(0, observability_1.emitNervesEvent)({
|
|
76
|
+
component: "friends",
|
|
77
|
+
event: "friends.audit_sink_init",
|
|
78
|
+
message: "file audit sink initialized",
|
|
79
|
+
meta: {},
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
async append(record) {
|
|
83
|
+
await fsPromises.appendFile(this.filePath, JSON.stringify(record) + "\n", "utf-8");
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
exports.FileAuditSink = FileAuditSink;
|