phaedo-mcp 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/NOTICE +17 -0
  2. package/README.md +51 -9
  3. package/action-binding.js +82 -0
  4. package/agreement.js +38 -0
  5. package/ap2.js +214 -0
  6. package/autonomy-promotion.js +280 -0
  7. package/build-package.mjs +25 -10
  8. package/capture-a11y.js +116 -0
  9. package/capture-local.js +85 -0
  10. package/capture-sync.js +99 -0
  11. package/consult-core.js +81 -19
  12. package/consult-policy.js +427 -33
  13. package/consult.js +25 -3
  14. package/delegation-sync.js +89 -0
  15. package/escalation-policy.sample.json +13 -0
  16. package/escalation.js +383 -0
  17. package/fingerprint-source.js +78 -4
  18. package/health.js +54 -0
  19. package/hooks/session-end.mjs +240 -0
  20. package/install.mjs +164 -21
  21. package/lib/capture-home.js +46 -0
  22. package/lib/domain.js +101 -0
  23. package/lib/is-main.js +40 -0
  24. package/lib/phaedo-crypto.js +17 -1
  25. package/package.json +26 -6
  26. package/pairing.js +5 -1
  27. package/phone-source.js +92 -12
  28. package/projection.js +4 -1
  29. package/pull.js +19 -3
  30. package/receipts-sync.js +197 -0
  31. package/receipts.js +69 -16
  32. package/server.js +305 -15
  33. package/server.json +4 -4
  34. package/signing-audit.js +111 -0
  35. package/signing-request.js +217 -0
  36. package/signing.js +176 -0
  37. package/vendor/authored-source.js +250 -0
  38. package/vendor/capabilities.js +203 -0
  39. package/vendor/capture/capture-core.js +67 -0
  40. package/vendor/capture/detectors.js +287 -0
  41. package/vendor/capture/exchanges.js +77 -0
  42. package/vendor/capture/model-detect.js +195 -0
  43. package/vendor/capture/owner.js +35 -0
  44. package/vendor/capture/package.json +4 -0
  45. package/vendor/capture/source-id.js +56 -0
  46. package/vendor/capture/state.js +148 -0
  47. package/vendor/capture/transcript.js +153 -0
  48. package/vendor/commitments.js +169 -0
  49. package/vendor/context-block.js +251 -18
  50. package/vendor/envelope.js +50 -4
  51. package/vendor/extraction/capture-combine.js +160 -0
  52. package/vendor/extraction/catalog.js +232 -0
  53. package/vendor/extraction/delta-lift.js +109 -0
  54. package/vendor/extraction/fingerprint-update.js +227 -0
  55. package/vendor/extraction/package.json +4 -0
  56. package/vendor/extraction/persist.js +40 -0
  57. package/vendor/extraction/reconcile.js +269 -0
  58. package/vendor/extraction/resolve.js +178 -0
  59. package/vendor/extraction/rule-promotion.js +137 -0
  60. package/vendor/kdf.js +20 -1
  61. package/vendor/protocol.js +7 -0
  62. package/vendored-files.mjs +82 -0
package/consult-core.js CHANGED
@@ -16,6 +16,8 @@
16
16
 
17
17
  import { PhaedoError } from './errors.js';
18
18
  import { loadPolicies, applyPolicies, matchAuthorization } from './consult-policy.js';
19
+ import { normalizeActionBinding, isBound } from './action-binding.js';
20
+ import { normalizeDomain, canonicalDomain } from './lib/domain.js';
19
21
 
20
22
  export const CONSULTATION_TYPES = [
21
23
  'action_approval',
@@ -367,14 +369,31 @@ export function normalizeRequest(request) {
367
369
  }
368
370
  const ctx = (request.context && typeof request.context === 'object') ? request.context : {};
369
371
  const ev = ctx.evidence_provided;
372
+ // §10.2 action_binding: the concrete call this descriptor claims to describe.
373
+ // Optional — an unbound request resolves exactly as before — but an authored
374
+ // authorization may refuse to RELAX anything without it (§10.5 require_binding).
375
+ const binding = normalizeActionBinding(request.action_binding);
370
376
  return {
371
377
  type,
372
- domain: typeof ad.domain === 'string' ? ad.domain : undefined,
378
+ // NORMALIZED only (case/whitespace) — deliberately NOT vocabulary-mapped. The
379
+ // request domain feeds §10.5 matching, and a relaxing rule must keep matching on
380
+ // the literal the agent sent; `domainCovers` applies the vocabulary itself, and
381
+ // only for restrictive rules. Mapping here instead would let an alias widen a
382
+ // grant. The RECEIPT stores the canonical form (that is the evidence axis).
383
+ domain: typeof ad.domain === 'string' ? (normalizeDomain(ad.domain) || undefined) : undefined,
373
384
  reversible: typeof ad.reversible === 'boolean' ? ad.reversible : undefined,
374
- magnitude: magnitude || 'medium',
385
+ // UNSTATED STAYS UNSTATED (v0.1.68). This defaulted to 'medium', which meant the resolver could
386
+ // never tell "the agent said medium" from "the agent said nothing" — and §10.5 then let a
387
+ // medium-capped grant fire on the nothing. The receipt now records what was actually sent
388
+ // (null), which is also what situational precedent (OQ24) needs to band on. The inferred
389
+ // oracle path still reads an unknown as the middle of the scale (`magnitudeRank`); that is a
390
+ // disposition heuristic, not a grant, and it is unchanged.
391
+ magnitude,
375
392
  amount: typeof ad.amount === 'number' && isFinite(ad.amount) ? ad.amount : undefined,
376
393
  summary: typeof ad.summary === 'string' ? ad.summary : undefined,
377
394
  evidenceProvided: Array.isArray(ev) ? ev : (ev != null ? [ev] : []),
395
+ binding,
396
+ bound: isBound(binding),
378
397
  };
379
398
  }
380
399
 
@@ -500,22 +519,50 @@ export function consultDrivers(vd, request) {
500
519
  // action's domain when it had one, so a financial override never reshapes writing.
501
520
  const PERMISSIVE_SIGNAL = new Set(['proceed', 'proceed_with_note']);
502
521
  const CAUTIOUS_SIGNAL = new Set(['escalate', 'decline', 'clarify']);
503
- const OVERRIDE_WEIGHT = { rejected: 1.0, modified: 0.5 };
522
+
523
+ // A delegated correction's signed weight — direction read from the subject's action
524
+ // RELATIVE to the oracle's signal, NOT the signal class alone. This is the fix for a
525
+ // direction inversion: a lesson requires DISAGREEMENT between signal and action.
526
+ // permissive + rejected → +1 said go, subject didn't proceed → wants CAUTION
527
+ // cautious + approved → -1 said hold, subject proceeded → wants BOLD
528
+ // modified → ±0.5 (ambiguous change; signal-relative, half weight)
529
+ // permissive + approved → 0 AGREEMENT (both proceed) — teaches nothing
530
+ // cautious + rejected → 0 AGREEMENT (both hold) — teaches nothing
531
+ // This keeps the anti-obedience rail intact (a rubber-stamp `approved` on a `proceed`
532
+ // signal is agreement, not a "be bolder" lesson) while no longer misreading the
533
+ // subject AGREEING with a cautious signal as a demand to be bolder — which was
534
+ // eroding the very guardrails the subject was upholding (agreement.js counts
535
+ // decline+rejected as agreement; this now matches).
536
+ function delegationCorrection(signal, userAction) {
537
+ const permissive = PERMISSIVE_SIGNAL.has(signal);
538
+ const cautious = CAUTIOUS_SIGNAL.has(signal);
539
+ if (!permissive && !cautious) return 0; // abstain / unknown signal
540
+ if (userAction === 'modified') return permissive ? 0.5 : -0.5;
541
+ if (permissive && userAction === 'rejected') return 1; // override toward caution
542
+ if (cautious && userAction === 'approved') return -1; // override toward bold (proceeded anyway)
543
+ return 0; // agreement / unknown → no lesson
544
+ }
504
545
 
505
546
  export function deriveDelegationSignals(receipts, { now = new Date().toISOString(), floor = 1 } = {}) {
506
547
  const tally = new Map(); // `${cue}|${domain}` → { cue, domain, sum, n }
507
548
  for (const r of (Array.isArray(receipts) ? receipts : [])) {
508
- const w = OVERRIDE_WEIGHT[r && r.user_action];
509
- if (!w) continue; // only rejected/modified teach
510
549
  const sig = r?.response?.signal;
511
- const correct = PERMISSIVE_SIGNAL.has(sig) ? 1 : CAUTIOUS_SIGNAL.has(sig) ? -1 : 0; // +1 cautious / -1 bold
512
- if (!correct) continue; // abstain/unknown → no direction
513
- const domain = (typeof r?.request?.action_descriptor?.domain === 'string') ? r.request.action_descriptor.domain : '';
550
+ const contribution = delegationCorrection(sig, r && r.user_action); // signed; 0 = agreement/no lesson
551
+ if (!contribution) continue;
552
+ // Canonicalize the domain BEFORE bucketing, or one concept spelled several ways
553
+ // (finance / financial / Finance) fragments into several near-identical buckets — the
554
+ // drift §10.2's published vocabulary exists to end. Both the live overlay
555
+ // (deriveDelegationSignals) and the promotion cards (buildDelegationPromotions) key on
556
+ // this, so both must canonicalize or a card floods / an overlay splits. Free text still
557
+ // survives — an unrecognized domain canonicalizes to itself — so only known synonyms
558
+ // merge, never distinct classes.
559
+ const domain = canonicalDomain(
560
+ (typeof r?.request?.action_descriptor?.domain === 'string') ? r.request.action_descriptor.domain : '');
514
561
  for (const d of (Array.isArray(r.drivers) ? r.drivers : [])) {
515
562
  if (!CUES[d && d.cue]) continue; // scalar caution cues only (ambiguity later)
516
563
  const k = `${d.cue}|${domain}`;
517
564
  const t = tally.get(k) || { cue: d.cue, domain, sum: 0, n: 0 };
518
- t.sum += correct * w; t.n += 1;
565
+ t.sum += contribution; t.n += 1;
519
566
  tally.set(k, t);
520
567
  }
521
568
  }
@@ -600,22 +647,28 @@ function delegationText(dimension, dir, domain) {
600
647
 
601
648
  export function buildDelegationPromotions(receipts, fp, { now = new Date().toISOString(), floor = DELEGATION_PROMOTION_FLOOR } = {}) {
602
649
  // Aggregate overrides per CANONICAL (dimension, domain): signed weight + count. Same
603
- // honesty rail + direction rule as deriveDelegationSignals (only rejected/modified;
604
- // direction read from the oracle's own signal).
650
+ // signal-relative direction rule as deriveDelegationSignals — a lesson requires the
651
+ // subject's action to DISAGREE with the oracle's signal; agreement teaches nothing.
605
652
  const tally = new Map(); // `${dimension}|${domain}` → { dimension, domain, sum, n }
606
653
  for (const r of (Array.isArray(receipts) ? receipts : [])) {
607
- const w = OVERRIDE_WEIGHT[r && r.user_action];
608
- if (!w) continue;
609
654
  const sig = r?.response?.signal;
610
- const correct = PERMISSIVE_SIGNAL.has(sig) ? 1 : CAUTIOUS_SIGNAL.has(sig) ? -1 : 0;
611
- if (!correct) continue;
612
- const domain = (typeof r?.request?.action_descriptor?.domain === 'string') ? r.request.action_descriptor.domain : '';
655
+ const contribution = delegationCorrection(sig, r && r.user_action);
656
+ if (!contribution) continue;
657
+ // Canonicalize the domain BEFORE bucketing, or one concept spelled several ways
658
+ // (finance / financial / Finance) fragments into several near-identical buckets — the
659
+ // drift §10.2's published vocabulary exists to end. Both the live overlay
660
+ // (deriveDelegationSignals) and the promotion cards (buildDelegationPromotions) key on
661
+ // this, so both must canonicalize or a card floods / an overlay splits. Free text still
662
+ // survives — an unrecognized domain canonicalizes to itself — so only known synonyms
663
+ // merge, never distinct classes.
664
+ const domain = canonicalDomain(
665
+ (typeof r?.request?.action_descriptor?.domain === 'string') ? r.request.action_descriptor.domain : '');
613
666
  for (const d of (Array.isArray(r.drivers) ? r.drivers : [])) {
614
667
  const dim = CUE_DIMENSION[d && d.cue];
615
668
  if (!dim) continue;
616
669
  const k = `${dim}|${domain}`;
617
670
  const t = tally.get(k) || { dimension: dim, domain, sum: 0, n: 0 };
618
- t.sum += correct * w; t.n += 1; tally.set(k, t);
671
+ t.sum += contribution; t.n += 1; tally.set(k, t);
619
672
  }
620
673
  }
621
674
  const keyOf = (dim, domain) => `${dim}|${domain || ''}`;
@@ -726,6 +779,15 @@ export function ruleResponse(vd, n) {
726
779
  return shape(n.type, resolved.signal, cal.confidence, resolved.hint, cal.deference_level);
727
780
  }
728
781
 
782
+ // The §10.5 evaluation context, lifted out of `opts` once so both public entries
783
+ // gate identically. `clientId` is the SERVING paired client's own `client_id` (from
784
+ // the server's `pairing.json`, supplied by the transport layer) — the identity
785
+ // `agents` scoping matches on; never anything the caller asserted. Absent here means
786
+ // unpaired, which makes every `agents`-scoped relaxing rule inert. Passing `now`
787
+ // through as well keeps rule expiry on the same clock `loadPolicies` already gives
788
+ // §5.7 commitments; both fall back to `Date.now()` when a caller omits them.
789
+ const policyCtx = (opts = {}) => ({ now: opts.now, clientId: opts.clientId ?? null });
790
+
729
791
  // ── Public pure entry ─────────────────────────────────────────────────────────
730
792
  // (vd, request[, opts]) -> §10.3 response. Deterministic; applies authored §10.5
731
793
  // policies last so a standing guardrail wins (even over an abstain). `opts.policies`
@@ -734,7 +796,7 @@ export function ruleResponse(vd, n) {
734
796
  export function resolveConsultationCore(vd, request = {}, opts = {}) {
735
797
  assertVaultUsable(vd);
736
798
  const n = normalizeRequest(request);
737
- return applyPolicies(ruleResponse(vd, n), n, loadPolicies(vd, opts));
799
+ return applyPolicies(ruleResponse(vd, n), n, loadPolicies(vd, opts), policyCtx(opts));
738
800
  }
739
801
 
740
802
  // Back-compat alias: the prior public name of the sync rule entry. Kept so existing
@@ -751,7 +813,7 @@ export const buildConsultationResponse = resolveConsultationCore;
751
813
  export function checkAuthorization(vd, request = {}, opts = {}) {
752
814
  assertVaultUsable(vd);
753
815
  const n = normalizeRequest(request);
754
- const auth = matchAuthorization(n, loadPolicies(vd, opts));
816
+ const auth = matchAuthorization(n, loadPolicies(vd, opts), policyCtx(opts));
755
817
  return {
756
818
  phaedo_response_version: '0.1',
757
819
  request_type: 'authorization_check',