@mindot/will 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/channels/discord.d.ts +1 -1
- package/dist/channels/discord.js.map +1 -1
- package/dist/channels/whatsapp.d.ts +1 -1
- package/dist/channels/whatsapp.js.map +1 -1
- package/dist/cli.js +998 -59
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +998 -59
- package/dist/index.js.map +1 -1
- package/dist/mcp/effectors.d.ts +1 -1
- package/dist/mcp/effectors.js.map +1 -1
- package/dist/{will-BDq-TMQr.d.ts → will-DAW0l-lY.d.ts} +90 -10
- package/package.json +1 -1
- package/src/channels/discord.ts +11 -11
- package/src/channels/roster.ts +1 -1
- package/src/channels/types.ts +1 -1
- package/src/channels/whatsapp.ts +25 -25
- package/src/cli.ts +20 -20
- package/src/cognition/acp.ts +28 -0
- package/src/cognition/agency/consequence.ts +174 -0
- package/src/cognition/agency/engines/action.selector.ts +333 -48
- package/src/cognition/agency/engines/affordance.synthesizer.ts +35 -16
- package/src/cognition/agency/engines/deliberation.engine.ts +28 -14
- package/src/cognition/agency/engines/motor.schema.executor.ts +90 -14
- package/src/cognition/agency/engines/reafference.engine.ts +152 -23
- package/src/cognition/agency/execution.primitives.ts +13 -13
- package/src/cognition/agency/proactive.communicator.ts +1 -1
- package/src/cognition/agency/reconcile.learning.ts +10 -1
- package/src/cognition/agency/revocation.ts +79 -0
- package/src/cognition/agency/schemas/repertoire.ts +109 -9
- package/src/cognition/agency/selection.scoring.ts +32 -10
- package/src/cognition/agency/types.ts +9 -0
- package/src/cognition/bus.ts +2 -2
- package/src/cognition/completion.inbox.ts +1 -1
- package/src/cognition/config.mirror.entities.ts +5 -5
- package/src/cognition/event.log.ts +2 -2
- package/src/cognition/event.schemas.ts +67 -67
- package/src/cognition/faculties/aesthetic.evaluator.ts +6 -6
- package/src/cognition/faculties/affective.blender.ts +36 -4
- package/src/cognition/faculties/attachment.evaluator.ts +6 -6
- package/src/cognition/faculties/attention.allocator.ts +63 -8
- package/src/cognition/faculties/autobiographical.narrator.ts +3 -3
- package/src/cognition/faculties/bias.detector.ts +7 -7
- package/src/cognition/faculties/circadian.oscillator.ts +3 -3
- package/src/cognition/faculties/confidence.calibrator.ts +7 -7
- package/src/cognition/faculties/dream.simulator.ts +5 -5
- package/src/cognition/faculties/empathy.simulator.ts +7 -7
- package/src/cognition/faculties/energy.regulator.ts +4 -4
- package/src/cognition/faculties/episodic.consolidator.ts +6 -6
- package/src/cognition/faculties/executive.engine/commands.ts +6 -6
- package/src/cognition/faculties/executive.engine/context.ts +42 -42
- package/src/cognition/faculties/executive.engine/deferred.effects.ts +2 -2
- package/src/cognition/faculties/executive.engine/engine.ts +25 -25
- package/src/cognition/faculties/executive.engine/facet.supervisor.ts +12 -12
- package/src/cognition/faculties/executive.engine/facet.ts +18 -18
- package/src/cognition/faculties/executive.engine/gating.ts +2 -2
- package/src/cognition/faculties/executive.engine/messages.ts +3 -3
- package/src/cognition/faculties/executive.engine/parser.ts +11 -11
- package/src/cognition/faculties/executive.engine/prompt.factory.ts +55 -55
- package/src/cognition/faculties/exteroception.ts +99 -5
- package/src/cognition/faculties/forgetting.curve.ts +3 -3
- package/src/cognition/faculties/frustration.evaluator.ts +6 -6
- package/src/cognition/faculties/goal.manager.ts +15 -15
- package/src/cognition/faculties/inhibition.controller.ts +3 -3
- package/src/cognition/faculties/interoception.ts +3 -3
- package/src/cognition/faculties/introspection.engine.ts +3 -3
- package/src/cognition/faculties/known.entity.tracker.ts +15 -15
- package/src/cognition/faculties/loss.evaluator.ts +2 -2
- package/src/cognition/faculties/moral.evaluator.ts +4 -4
- package/src/cognition/faculties/novelty.detector.ts +4 -4
- package/src/cognition/faculties/persona.consolidator.ts +12 -12
- package/src/cognition/faculties/planning.engine/engine.ts +47 -47
- package/src/cognition/faculties/planning.engine/plan.frontier.ts +5 -5
- package/src/cognition/faculties/planning.engine/plan.store.ts +1 -1
- package/src/cognition/faculties/planning.engine/plan.supervision.ts +20 -20
- package/src/cognition/faculties/reputation.tracker.ts +6 -6
- package/src/cognition/faculties/reward.evaluator.ts +4 -4
- package/src/cognition/faculties/self.model.updater.ts +8 -8
- package/src/cognition/faculties/semantic.engine/integrator.ts +15 -15
- package/src/cognition/faculties/sleep.pressure.regulator.ts +4 -4
- package/src/cognition/faculties/social.perception.ts +3 -3
- package/src/cognition/faculties/spaced.repetition.ts +2 -2
- package/src/cognition/faculties/stress.regulator.ts +27 -4
- package/src/cognition/faculties/task.switcher.ts +11 -6
- package/src/cognition/faculties/theory.of.mind.ts +6 -6
- package/src/cognition/faculties/threat.evaluator.ts +3 -3
- package/src/cognition/faculties/working.memory.ts +8 -8
- package/src/cognition/generative.model.ts +2 -2
- package/src/cognition/memory/vector.adapter.ts +5 -5
- package/src/cognition/memory/vector.content.ts +4 -4
- package/src/cognition/memory/vector.embedder.ts +9 -9
- package/src/cognition/persona.prior.ts +1 -1
- package/src/cognition/schema.registry.ts +2 -2
- package/src/cognition/senses/audition.engine/engine.ts +2 -2
- package/src/cognition/senses/base.sense.engine.ts +1 -1
- package/src/cognition/utilities/token.tracker.ts +5 -5
- package/src/core/abstracts.ts +1 -1
- package/src/core/completion.recorder.ts +2 -2
- package/src/core/serialization.ts +3 -3
- package/src/core/snapshot.manager.ts +1 -1
- package/src/core/state.manager.ts +1 -1
- package/src/core/utils.ts +1 -1
- package/src/host/boot.ts +30 -30
- package/src/host/utterances.ts +1 -1
- package/src/llm/index.ts +9 -9
- package/src/llm/summarizer.ts +4 -4
- package/src/llm/wire.contracts.ts +1 -1
- package/src/mcp/effectors.ts +10 -10
- package/src/mcp/server.ts +11 -11
- package/src/pma/eval.ts +1 -1
- package/src/pma/index.ts +21 -21
- package/src/runners/outreach.runner.ts +3 -3
- package/src/runners/social.runner.ts +1 -1
- package/src/runners/thin-shim.runner.ts +1 -1
- package/src/sdk/will.ts +5 -5
- package/src/serve/server.ts +12 -12
- package/src/stem/assembly.audit.ts +3 -3
- package/src/stem/guards/identity.coherence.ts +10 -10
- package/src/stem/guards/identity.guard.ts +22 -22
- package/src/stem/index.ts +26 -11
- package/src/stem/mind.ts +12 -12
- package/src/stem/policy/arbiter.ts +136 -0
- package/src/stem/policy/rule.table.ts +172 -0
- package/src/stem/policy/verdict.recorder.ts +0 -0
- package/src/stem/tracts/biography.writer.ts +3 -3
- package/src/stem/tracts/effector.controller.ts +383 -4
- package/src/stem/tracts/health.reporter.ts +1 -1
- package/src/stem/tracts/outbox.controller.ts +4 -0
- package/src/stem/tracts/outbox.writer.ts +4 -4
- package/src/stem/tracts/pma.controller.ts +1 -1
- package/src/stem/tracts/replay.controller.ts +10 -10
- package/src/stem/tracts/sensory.controller.ts +2 -2
- package/src/stem/tracts/session.logger.ts +4 -4
- package/src/stem/tracts/transport/socketio.transport.ts +7 -7
- package/src/stem/tracts/transport/stream.transport.ts +2 -2
- package/src/stem/tracts/transport.controller.ts +15 -15
package/src/stem/mind.ts
CHANGED
|
@@ -409,10 +409,10 @@ export function _resolveVectorMemory(
|
|
|
409
409
|
const mockMode = process.env.WILL_VECTOR_MEMORY === 'mock'
|
|
410
410
|
const rawModel = embeddingModel
|
|
411
411
|
?? process.env.WILL_EMBEDDING_MODEL
|
|
412
|
-
?? ( process.env.WILL_EMBEDDING_API_KEY ? 'text-embedding-3-small' : 'none'
|
|
412
|
+
?? ( process.env.WILL_EMBEDDING_API_KEY ? 'text-embedding-3-small' : 'none')
|
|
413
413
|
|
|
414
414
|
// Explicitly disabled — the documented "none" sentinel or recall turned off.
|
|
415
|
-
if( !mockMode && ( rawModel === 'none' || process.env.WILL_SEMANTIC_RECALL === 'false'
|
|
415
|
+
if( !mockMode && ( rawModel === 'none' || process.env.WILL_SEMANTIC_RECALL === 'false') )
|
|
416
416
|
return { embedder: null, vectorMemory: null }
|
|
417
417
|
|
|
418
418
|
// testMode promises a deterministic, zero-key, offline mind — but a dev .env
|
|
@@ -581,9 +581,9 @@ export function assembleMind( willId: string, config: WillConfig ): MindAssembly
|
|
|
581
581
|
profileContext: profile?.context,
|
|
582
582
|
})
|
|
583
583
|
if( !idGuard.ok )
|
|
584
|
-
throw new Error(
|
|
584
|
+
throw new Error(`Invalid Will identity for "${willId}": ${ idGuard.errors.join('; ') }`)
|
|
585
585
|
for( const w of idGuard.warnings )
|
|
586
|
-
logger.warn(
|
|
586
|
+
logger.warn(`[identity-guard] ${willId}: ${w}`)
|
|
587
587
|
config = { ...config, identity: idGuard.sanitized.identity }
|
|
588
588
|
|
|
589
589
|
// ── Construct ────────────────────────────────────────────
|
|
@@ -602,8 +602,8 @@ export function assembleMind( willId: string, config: WillConfig ): MindAssembly
|
|
|
602
602
|
// sessionLogger/grants); tests/unit/assembly.order.test.ts pins that set per
|
|
603
603
|
// tier, so a NEW unwired attachment fails loudly in CI, not here.
|
|
604
604
|
for( const rec of auditAssemblyWiring( simulation.orchestrator.engines ) )
|
|
605
|
-
if( rec.status === 'unwired'
|
|
606
|
-
logger.debug(
|
|
605
|
+
if( rec.status === 'unwired')
|
|
606
|
+
logger.debug(`[assembly] ${willId}: ${rec.engine}.${rec.method} unwired at assembly (anatomy=${anatomy})`)
|
|
607
607
|
|
|
608
608
|
// ── Seed readable simulation state ───────────────────────
|
|
609
609
|
// Identity, optional initial goals, and the engine-config mirror.
|
|
@@ -772,7 +772,7 @@ function _constructCognition(
|
|
|
772
772
|
// ── Planning Engine ──────────────────────────────────────
|
|
773
773
|
const planningEngine = new PlanningEngine()
|
|
774
774
|
planningEngine.attachGoalManager( goalManager )
|
|
775
|
-
if( anatomy !== 'reflex'
|
|
775
|
+
if( anatomy !== 'reflex') planningEngine.attachExecutiveEngine( executiveEngine )
|
|
776
776
|
executiveEngine.attachPlanningEngine( planningEngine )
|
|
777
777
|
|
|
778
778
|
// ── Executive ────────────────────────────────────────────
|
|
@@ -791,7 +791,7 @@ function _constructCognition(
|
|
|
791
791
|
autobiographicalNarrator.attachEpisodicConsolidator( episodicConsolidator )
|
|
792
792
|
autobiographicalNarrator.attachSemanticIntegrator( semanticIntegrator )
|
|
793
793
|
// Satellites harvest the executive's own output — attach wherever it runs.
|
|
794
|
-
if( anatomy !== 'reflex'
|
|
794
|
+
if( anatomy !== 'reflex'){
|
|
795
795
|
autobiographicalNarrator.attachExecutiveEngine( executiveEngine )
|
|
796
796
|
introspectionEngine.attachExecutiveEngine( executiveEngine )
|
|
797
797
|
}
|
|
@@ -807,7 +807,7 @@ function _constructCognition(
|
|
|
807
807
|
empathySimulator.attachTheoryOfMind( theoryOfMind )
|
|
808
808
|
|
|
809
809
|
// ── Context compaction components (mind anatomy only) ──
|
|
810
|
-
if( anatomy !== 'reflex'
|
|
810
|
+
if( anatomy !== 'reflex'){
|
|
811
811
|
const summarizer = new ExecutiveSummarizer({
|
|
812
812
|
summaryInterval: parseInt( process.env.WILL_SUMMARY_INTERVAL ?? '10'),
|
|
813
813
|
bufferSize: parseInt( process.env.WILL_SUMMARY_BUFFER_SIZE ?? '12'),
|
|
@@ -845,7 +845,7 @@ function _constructCognition(
|
|
|
845
845
|
const olfactionEngine = new OlfactionEngine()
|
|
846
846
|
const gustationEngine = new GustationEngine()
|
|
847
847
|
|
|
848
|
-
if( anatomy !== 'reflex'
|
|
848
|
+
if( anatomy !== 'reflex')
|
|
849
849
|
auditionEngine.attachExecutiveEngine( executiveEngine )
|
|
850
850
|
|
|
851
851
|
// §5.4 — cold-spawn digest hydration: on the first turn for an entity, seed an
|
|
@@ -906,7 +906,7 @@ function _constructCognition(
|
|
|
906
906
|
// winner (graceful System-1 degradation).
|
|
907
907
|
const deliberationEngine = new DeliberationEngine()
|
|
908
908
|
deliberationEngine.setWillName( config.name )
|
|
909
|
-
if( anatomy !== 'reflex'
|
|
909
|
+
if( anatomy !== 'reflex')
|
|
910
910
|
deliberationEngine.attachExecutive( executiveEngine )
|
|
911
911
|
|
|
912
912
|
// ── Build Cognition ──────────────────────────────────────
|
|
@@ -1108,7 +1108,7 @@ function _seedIdentity(
|
|
|
1108
1108
|
// to infer its identity from context. config.name is the canonical source of truth.
|
|
1109
1109
|
const nameAlreadyInPrompt = personaText.toLowerCase().includes( config.name.toLowerCase() )
|
|
1110
1110
|
const namePrefix = nameAlreadyInPrompt ? '' : `I am ${config.name}.`
|
|
1111
|
-
const fullPersonaText = [ namePrefix, personaText ].filter( Boolean ).join(
|
|
1111
|
+
const fullPersonaText = [ namePrefix, personaText ].filter( Boolean ).join(' ')
|
|
1112
1112
|
|
|
1113
1113
|
const prompt = [
|
|
1114
1114
|
WILL_CORE_PREAMBLE,
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// ─────────────────────────────────────────────────────────────
|
|
2
|
+
// src/stem/policy/arbiter.ts — the policy seam (PEP side)
|
|
3
|
+
// ─────────────────────────────────────────────────────────────
|
|
4
|
+
//
|
|
5
|
+
// POLICY_REAFFERENCE P0. A Policy Decision Point is consulted before a
|
|
6
|
+
// host-owned effector invocation is handed to the world. This file is the
|
|
7
|
+
// *interface* only — the enforcement point lives in effectorController
|
|
8
|
+
// (`bufferInvocation`), which is the single tract every external effect
|
|
9
|
+
// already passes through.
|
|
10
|
+
//
|
|
11
|
+
// Deliberately provider-agnostic: no transport, no vendor types, no Go. The
|
|
12
|
+
// null arbiter below is the default and is a strict no-op, so a Will with no
|
|
13
|
+
// policy configured runs byte-identically to one built before this file
|
|
14
|
+
// existed. `RuleTableArbiter` (rule.table.ts) is the local reference adapter;
|
|
15
|
+
// an external PDP (e.g. helm-ai-kernel) is a later phase and must implement
|
|
16
|
+
// nothing more than this interface.
|
|
17
|
+
//
|
|
18
|
+
// WHY THE STEM AND NOT COGNITION: the mind must never meet a permission
|
|
19
|
+
// dialog. It dispatches an intent and the world either yields or resists —
|
|
20
|
+
// a refusal arrives as reafference, in the same currency as any other
|
|
21
|
+
// outcome. Keeping the arbiter below the SDK and outside the cognition layer
|
|
22
|
+
// is what preserves that (and is the honest model besides: a body that
|
|
23
|
+
// cannot do the thing).
|
|
24
|
+
//
|
|
25
|
+
// SYNC OR ASYNC: `evaluate` may return a Verdict or a Promise of one. An
|
|
26
|
+
// external PDP will be async, and that is fine by construction — the intent
|
|
27
|
+
// is already held 'awaiting' for AWAIT_TIMEOUT (15 ticks) by the executor, so
|
|
28
|
+
// arbiter latency is absorbed by machinery that already exists. Both P0
|
|
29
|
+
// adapters are synchronous, which is why P0 changes no behaviour.
|
|
30
|
+
// ─────────────────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
/** What the boundary decided about a proposed effect. */
|
|
33
|
+
export type PolicyDecision = 'allow' | 'deny' | 'escalate'
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Is this denial a property of the ACTION CLASS (never permitted under the
|
|
37
|
+
* active policy) or of THIS INSTANCE (wrong target, exceeded bound)?
|
|
38
|
+
*
|
|
39
|
+
* This is the distinction that makes a refusal learnable rather than a wall to
|
|
40
|
+
* re-probe forever, and it is the field we are proposing upstream to HELM. A
|
|
41
|
+
* 'class' denial should suppress the affordance; an 'instance' denial should
|
|
42
|
+
* only narrow the parameter envelope the Will reaches for.
|
|
43
|
+
*/
|
|
44
|
+
export type DenialFinality = 'class' | 'instance'
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The nearest allowed envelope — what WOULD have been permitted. Structured so
|
|
48
|
+
* a learner can consume it; `field` names the constraint that bit.
|
|
49
|
+
*
|
|
50
|
+
* Every denial branch of a policy evaluator already computes this and usually
|
|
51
|
+
* discards it. We keep it.
|
|
52
|
+
*/
|
|
53
|
+
export interface PolicyCounterfactual {
|
|
54
|
+
/** The constrained field, e.g. 'ttl_days', 'target', 'amount'. */
|
|
55
|
+
field: string
|
|
56
|
+
/** What the Will asked for. */
|
|
57
|
+
requested?: unknown
|
|
58
|
+
/** The bound or permitted set, e.g. 30, or [ 'a', 'b' ]. */
|
|
59
|
+
allowed?: unknown
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** A boundary decision about one proposed invocation. */
|
|
63
|
+
export interface Verdict {
|
|
64
|
+
decision: PolicyDecision
|
|
65
|
+
/** Stable machine-readable code, e.g. 'TARGET_NOT_ALLOWED'. Never prose. */
|
|
66
|
+
reasonCode?: string
|
|
67
|
+
/** Meaningful on 'deny' only. Absent ⇒ treat as 'instance' (the safe default:
|
|
68
|
+
* it narrows rather than suppresses, so a mis-tagged denial cannot silently
|
|
69
|
+
* delete an ability from the Will's reach). */
|
|
70
|
+
finality?: DenialFinality
|
|
71
|
+
counterfactual?: PolicyCounterfactual
|
|
72
|
+
/** Free-text for logs and host UX. NEVER parsed by cognition. */
|
|
73
|
+
detail?: string
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The proposed effect, as the stem sees it. Mirrors the `agency.invocation`
|
|
78
|
+
* bus payload the MotorSchemaExecutor emits — no cognitive internals cross
|
|
79
|
+
* this boundary, only the act itself.
|
|
80
|
+
*/
|
|
81
|
+
export interface PolicyInvocation {
|
|
82
|
+
willId: string
|
|
83
|
+
/** The awaiting `agency.intent` id — the correlation handle, end to end. */
|
|
84
|
+
intentId: string
|
|
85
|
+
/** The motor schema id, i.e. the ability being enacted. */
|
|
86
|
+
schema: string
|
|
87
|
+
parameters: Record<string, unknown>
|
|
88
|
+
targetEntityId?: string
|
|
89
|
+
/** The ability's declared meaning, as given by the host at wiring time. */
|
|
90
|
+
description?: string
|
|
91
|
+
tick: number
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* A Policy Decision Point. Implementations must be PURE with respect to the
|
|
96
|
+
* Will: an arbiter may read its own policy and the invocation, and nothing
|
|
97
|
+
* else. It must not reach into simulation state.
|
|
98
|
+
*
|
|
99
|
+
* DETERMINISM CONTRACT: an arbiter is an external oracle, exactly like the LLM.
|
|
100
|
+
* Its verdicts are recorded on the tape and replayed back — replay never
|
|
101
|
+
* re-consults an arbiter (see P1). Implementations therefore need not be
|
|
102
|
+
* deterministic themselves, but MUST be free of side effects on the Will.
|
|
103
|
+
*/
|
|
104
|
+
export interface PolicyArbiter {
|
|
105
|
+
/** Stable identifier, recorded alongside the verdict for audit. */
|
|
106
|
+
readonly name: string
|
|
107
|
+
evaluate( invocation: PolicyInvocation ): Verdict | Promise<Verdict>
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** The single allow verdict, frozen and shared — the null arbiter allocates nothing. */
|
|
111
|
+
const ALLOW: Readonly<Verdict> = Object.freeze({ decision: 'allow' as const })
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* The default. Allows everything, allocates nothing, logs nothing.
|
|
115
|
+
*
|
|
116
|
+
* A Will running this must be byte-identical to one built before the policy
|
|
117
|
+
* seam existed — that property is asserted by test, and it is what lets this
|
|
118
|
+
* ship dark.
|
|
119
|
+
*/
|
|
120
|
+
export const NULL_ARBITER: PolicyArbiter = {
|
|
121
|
+
name: 'null',
|
|
122
|
+
evaluate(): Verdict { return ALLOW },
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** True when the arbiter is the no-op default (used to skip the seam entirely). */
|
|
126
|
+
export function isNullArbiter( arbiter: PolicyArbiter | null | undefined ): boolean {
|
|
127
|
+
return !arbiter || arbiter === NULL_ARBITER
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Normalize a denial's finality. Absent ⇒ 'instance', the conservative reading:
|
|
132
|
+
* an unlabelled refusal narrows the envelope but never removes the ability.
|
|
133
|
+
*/
|
|
134
|
+
export function finalityOf( verdict: Verdict ): DenialFinality {
|
|
135
|
+
return verdict.finality === 'class' ? 'class' : 'instance'
|
|
136
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
// ─────────────────────────────────────────────────────────────
|
|
2
|
+
// src/stem/policy/rule.table.ts — the local reference PDP
|
|
3
|
+
// ─────────────────────────────────────────────────────────────
|
|
4
|
+
//
|
|
5
|
+
// POLICY_REAFFERENCE P0. A declarative, in-process arbiter: enough policy to
|
|
6
|
+
// prove the seam is sufficient WITHOUT an external dependency, and the
|
|
7
|
+
// fallback if an external-PDP track stalls.
|
|
8
|
+
//
|
|
9
|
+
// It is also a working demonstration of the two receipt fields we are
|
|
10
|
+
// proposing upstream (`finality`, `counterfactual`): when a bound is violated
|
|
11
|
+
// this arbiter *returns what would have been allowed* instead of discarding
|
|
12
|
+
// it. That is the whole argument, implemented in ~40 lines — a denial that
|
|
13
|
+
// says "never" and one that says "not with these parameters" are different
|
|
14
|
+
// facts, and a learner needs to tell them apart.
|
|
15
|
+
//
|
|
16
|
+
// Evaluation is first-match-wins over an ordered rule array, so a verdict is a
|
|
17
|
+
// pure function of ( rules, invocation ) — no clock, no IO, no iteration-order
|
|
18
|
+
// surprises. Deterministic by construction, which keeps replay honest.
|
|
19
|
+
// ─────────────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
import type {
|
|
22
|
+
PolicyArbiter, PolicyInvocation, Verdict,
|
|
23
|
+
PolicyDecision, DenialFinality, PolicyCounterfactual,
|
|
24
|
+
} from '#stem/policy/arbiter'
|
|
25
|
+
|
|
26
|
+
/** A bound on one parameter. Checked in declaration order: max, min, equals, oneOf. */
|
|
27
|
+
export interface ParamConstraint {
|
|
28
|
+
max?: number
|
|
29
|
+
min?: number
|
|
30
|
+
equals?: unknown
|
|
31
|
+
oneOf?: readonly unknown[]
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* One rule. `schema`/`target` scope it (omitted ⇒ matches any); the FIRST rule
|
|
36
|
+
* whose scope matches decides, so order is policy.
|
|
37
|
+
*
|
|
38
|
+
* `require` is meaningful with `decision: 'allow'` only: the scope matched, and
|
|
39
|
+
* these constraints must hold for the allow to stand. A violation flips the
|
|
40
|
+
* verdict to deny — carrying the counterfactual — with finality 'instance',
|
|
41
|
+
* because the ability itself was permitted and only these arguments were not.
|
|
42
|
+
*
|
|
43
|
+
* A rule with `decision: 'deny'` and no `require` is a flat class-level ban;
|
|
44
|
+
* it reports finality 'class' unless told otherwise.
|
|
45
|
+
*/
|
|
46
|
+
export interface PolicyRule {
|
|
47
|
+
schema?: string
|
|
48
|
+
target?: string
|
|
49
|
+
decision: PolicyDecision
|
|
50
|
+
require?: Record<string, ParamConstraint>
|
|
51
|
+
reasonCode?: string
|
|
52
|
+
finality?: DenialFinality
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface RuleTableOptions {
|
|
56
|
+
rules: readonly PolicyRule[]
|
|
57
|
+
/**
|
|
58
|
+
* The verdict when NO rule matches. Required — deliberately not defaulted.
|
|
59
|
+
* A policy component that silently defaults open is a trap; make the posture
|
|
60
|
+
* an explicit decision at the call site. 'deny' is fail-closed and is the
|
|
61
|
+
* right choice once a rule set is complete.
|
|
62
|
+
*/
|
|
63
|
+
fallthrough: PolicyDecision
|
|
64
|
+
/** Recorded with every verdict for audit. Defaults to 'rule-table'. */
|
|
65
|
+
name?: string
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export class RuleTableArbiter implements PolicyArbiter {
|
|
69
|
+
readonly name: string
|
|
70
|
+
private readonly _rules: readonly PolicyRule[]
|
|
71
|
+
private readonly _fallthrough: PolicyDecision
|
|
72
|
+
|
|
73
|
+
constructor( opts: RuleTableOptions ){
|
|
74
|
+
this.name = opts.name ?? 'rule-table'
|
|
75
|
+
this._rules = opts.rules
|
|
76
|
+
this._fallthrough = opts.fallthrough
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
evaluate( invocation: PolicyInvocation ): Verdict {
|
|
80
|
+
for( const rule of this._rules ){
|
|
81
|
+
if( !scopeMatches( rule, invocation ) ) continue
|
|
82
|
+
|
|
83
|
+
if( rule.decision !== 'allow')
|
|
84
|
+
return {
|
|
85
|
+
decision: rule.decision,
|
|
86
|
+
...( rule.reasonCode ? { reasonCode: rule.reasonCode } : {} ),
|
|
87
|
+
...( rule.decision === 'deny'
|
|
88
|
+
? { finality: rule.finality ?? 'class' }
|
|
89
|
+
: {} ),
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const violation = firstViolation( rule.require, invocation.parameters )
|
|
93
|
+
if( violation )
|
|
94
|
+
return {
|
|
95
|
+
decision: 'deny',
|
|
96
|
+
reasonCode: rule.reasonCode ?? violation.reasonCode,
|
|
97
|
+
finality: rule.finality ?? 'instance',
|
|
98
|
+
counterfactual: violation.counterfactual,
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return { decision: 'allow' }
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
decision: this._fallthrough,
|
|
106
|
+
...( this._fallthrough !== 'allow' ? { reasonCode: 'NO_MATCHING_RULE', finality: 'class' as const } : {} ),
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ── matching ─────────────────────────────────────────────────────────────────
|
|
112
|
+
|
|
113
|
+
function scopeMatches( rule: PolicyRule, inv: PolicyInvocation ): boolean {
|
|
114
|
+
if( rule.schema !== undefined && rule.schema !== '*' && rule.schema !== inv.schema ) return false
|
|
115
|
+
if( rule.target !== undefined && rule.target !== '*' && rule.target !== inv.targetEntityId ) return false
|
|
116
|
+
return true
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
interface Violation {
|
|
120
|
+
reasonCode: string
|
|
121
|
+
counterfactual: PolicyCounterfactual
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* The first constraint that fails, in declared order — so the counterfactual a
|
|
126
|
+
* caller receives is stable, not whichever check happened to run first.
|
|
127
|
+
*
|
|
128
|
+
* An ABSENT parameter is a violation: a bound cannot be honoured by a value
|
|
129
|
+
* that was never supplied, and habitual enaction (which carries no args) must
|
|
130
|
+
* not slip past a constraint the deliberate path would have to satisfy.
|
|
131
|
+
*/
|
|
132
|
+
function firstViolation(
|
|
133
|
+
require: Record<string, ParamConstraint> | undefined,
|
|
134
|
+
parameters: Record<string, unknown>,
|
|
135
|
+
): Violation | null {
|
|
136
|
+
if( !require ) return null
|
|
137
|
+
|
|
138
|
+
for( const [ field, constraint ] of Object.entries( require ) ){
|
|
139
|
+
const present = Object.prototype.hasOwnProperty.call( parameters, field )
|
|
140
|
+
if( !present )
|
|
141
|
+
return {
|
|
142
|
+
reasonCode: 'PARAM_MISSING',
|
|
143
|
+
counterfactual: { field, allowed: describe( constraint ) },
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const value = parameters[ field ]
|
|
147
|
+
|
|
148
|
+
if( constraint.max !== undefined && !( typeof value === 'number' && value <= constraint.max ) )
|
|
149
|
+
return { reasonCode: 'PARAM_ABOVE_MAX', counterfactual: { field, requested: value, allowed: constraint.max } }
|
|
150
|
+
|
|
151
|
+
if( constraint.min !== undefined && !( typeof value === 'number' && value >= constraint.min ) )
|
|
152
|
+
return { reasonCode: 'PARAM_BELOW_MIN', counterfactual: { field, requested: value, allowed: constraint.min } }
|
|
153
|
+
|
|
154
|
+
if( 'equals' in constraint && value !== constraint.equals )
|
|
155
|
+
return { reasonCode: 'PARAM_NOT_EQUAL', counterfactual: { field, requested: value, allowed: constraint.equals } }
|
|
156
|
+
|
|
157
|
+
if( constraint.oneOf !== undefined && !constraint.oneOf.includes( value ) )
|
|
158
|
+
return { reasonCode: 'PARAM_NOT_IN_SET', counterfactual: { field, requested: value, allowed: [ ...constraint.oneOf ] } }
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return null
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** A compact description of a constraint, for the missing-parameter case. */
|
|
165
|
+
function describe( c: ParamConstraint ): unknown {
|
|
166
|
+
if( c.oneOf !== undefined ) return [ ...c.oneOf ]
|
|
167
|
+
if( 'equals' in c ) return c.equals
|
|
168
|
+
if( c.max !== undefined && c.min !== undefined ) return { min: c.min, max: c.max }
|
|
169
|
+
if( c.max !== undefined ) return { max: c.max }
|
|
170
|
+
if( c.min !== undefined ) return { min: c.min }
|
|
171
|
+
return null
|
|
172
|
+
}
|
|
Binary file
|
|
@@ -38,15 +38,15 @@ export class BiographyWriter {
|
|
|
38
38
|
if( !fileLoggingEnabled() ) return
|
|
39
39
|
|
|
40
40
|
const dataDir = process.env[ 'WILL_DATA_DIR' ] ?? './data'
|
|
41
|
-
const dir = join( dataDir, 'wills', willId, 'profiles'
|
|
41
|
+
const dir = join( dataDir, 'wills', willId, 'profiles')
|
|
42
42
|
try {
|
|
43
43
|
if( !this._ensuredDirs.has( dir ) ){
|
|
44
44
|
mkdirSync( dir, { recursive: true } )
|
|
45
45
|
this._ensuredDirs.add( dir )
|
|
46
46
|
}
|
|
47
|
-
appendFileSync( join( dir, file ), JSON.stringify( record ) + '\n', 'utf8'
|
|
47
|
+
appendFileSync( join( dir, file ), JSON.stringify( record ) + '\n', 'utf8')
|
|
48
48
|
} catch( err ){
|
|
49
|
-
logger.error(
|
|
49
|
+
logger.error(`[biography] ${file} write failed:`, err )
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
52
|
/**
|