@xshieldai/hanumang-mandate 0.2.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/LICENSE ADDED
@@ -0,0 +1,31 @@
1
+ GNU AFFERO GENERAL PUBLIC LICENSE
2
+ Version 3, 19 November 2007
3
+
4
+ Copyright (C) 2026 ANKR Labs / Capt. Anil Sharma
5
+
6
+ This program is free software: you can redistribute it and/or modify
7
+ it under the terms of the GNU Affero General Public License as published by
8
+ the Free Software Foundation, either version 3 of the License, or
9
+ (at your option) any later version.
10
+
11
+ This program is distributed in the hope that it will be useful,
12
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ GNU Affero General Public License for more details.
15
+
16
+ You should have received a copy of the GNU Affero General Public License
17
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
18
+
19
+ ---
20
+
21
+ The full text of the GNU Affero General Public License v3 is available at:
22
+ https://www.gnu.org/licenses/agpl-3.0.txt
23
+
24
+ ADDITIONAL TERMS (permitted under AGPL §7):
25
+
26
+ If you run a modified version of this software as a network service,
27
+ you must make the complete source code of the modified version available
28
+ to all users of that service under the terms of this license.
29
+
30
+ Commercial use, including SaaS deployments and enterprise integrations,
31
+ requires a separate commercial license. Contact: captain@ankr.in
package/README.md ADDED
@@ -0,0 +1,251 @@
1
+ # @rocketlang/hanumang-mandate
2
+
3
+ Agent delegation credential verifier + 7-axis posture scorer. Pure primitives extracted from the internal **xshieldai-hanumang** Fastify service.
4
+
5
+ **Two primitives. No DB. No HTTP. Install and use.**
6
+
7
+ ## What this is
8
+
9
+ `hanumang-mandate` is the credential + posture-scoring layer of HanumanG, the agent-delegation-posture monitor inside xShieldAI. The full service has SQLite-backed attestations, regression alerts, revocation-URL polling, and Forja endpoints — that lives in the closed product. This package is the two primitives the rest of the service is built on: **Mudrika credential verification** and **7-axis posture scoring**.
10
+
11
+ ## Complementary to `@rocketlang/aegis` HanumanG
12
+
13
+ The aegis package and this package are **different governance moments**, both named "HanumanG":
14
+
15
+ | | `@rocketlang/aegis` HanumanG | `@rocketlang/hanumang-mandate` |
16
+ |---|---|---|
17
+ | Question | *Can this agent SPAWN?* | *Is this agent's MANDATE valid? What's its posture?* |
18
+ | When | PreToolUse hook (spawn-time) | Continuous (per-action) |
19
+ | Output | Binary PASS/FAIL | A/B/C/D/F grade + per-axis score |
20
+ | Axes (7) | identity / authorization / scope / budget / depth / purpose / revocability | mudrika_integrity / identity_broadcast / mandate_bounds / proportional_force / return_with_proof / no_overreach / truthful_report |
21
+
22
+ Use both. They are **not duplicates** — they cover different governance concerns.
23
+
24
+ ## Install
25
+
26
+ ```bash
27
+ npm install @rocketlang/hanumang-mandate
28
+ # or
29
+ bun add @rocketlang/hanumang-mandate
30
+ ```
31
+
32
+ ## Mudrika — the delegation credential
33
+
34
+ A Mudrika is a JWT-shaped credential that a principal issues to an agent. It declares: who is acting, on whose behalf, for what task, with what trust mask, in what scope, for how long, with what proof of provenance.
35
+
36
+ ```typescript
37
+ import { verifyMudrika } from '@rocketlang/hanumang-mandate';
38
+
39
+ const mudrika = {
40
+ mudrika_version: 'v1',
41
+ mudrika_id: 'mdr-001-2026-05-16',
42
+ principal_id: 'user:capt-anil',
43
+ agent_id: 'agent:codex-001',
44
+ task_id: 'task:refactor-routes',
45
+ trust_mask: 0b00011111, // 5 bits set
46
+ scope_key: 'aegis/packages/aegis-guard',
47
+ issued_at: '2026-05-16T12:00:00Z',
48
+ ttl_seconds: 3600,
49
+ required_return_proof: 'pramana_receipt',
50
+ revocation_url: 'https://aegis.rocketlang.dev/mudrika/revoke',
51
+ pramana_chain: ['root', 'pramana:abc123'],
52
+ };
53
+
54
+ const result = verifyMudrika(mudrika, 'agent:codex-001');
55
+ // {
56
+ // outcome: 'PASS', // or 'FAIL' / 'EXPIRED' / 'REVOKED'
57
+ // failure_reason: null,
58
+ // expires_at: '2026-05-16T13:00:00Z',
59
+ // trust_mask: 31,
60
+ // scope_key: 'aegis/packages/aegis-guard',
61
+ // principal_id: 'user:capt-anil',
62
+ // ...
63
+ // }
64
+ ```
65
+
66
+ ### Phase-1 limit — signature is NOT cryptographically verified
67
+
68
+ `verifyMudrika()` validates structure + TTL + trust_mask range. It does **not** verify the `signature` field cryptographically. The `signature` is in the payload schema for forward compatibility; today, callers must establish provenance themselves (e.g., authenticated transport, internal trust boundary).
69
+
70
+ Phase 2 will add signature verification. If you need it now, wrap `verifyMudrika()` with your own crypto check.
71
+
72
+ ## 7-axis posture scorer
73
+
74
+ The scorer assesses an agent's per-action behaviour across seven axes. Each axis returns 0–100 + an outcome (`PASS` / `WARN` / `FAIL`). The aggregate `PostureScore` uses a **worst-axis floor** (`HNG-YK-001`) — a single FAIL caps the grade at D regardless of how high the average is.
75
+
76
+ ```typescript
77
+ import { scoreAxis, computePostureScore } from '@rocketlang/hanumang-mandate';
78
+
79
+ const axisScores = [
80
+ scoreAxis({
81
+ axis: 'mudrika_integrity',
82
+ mudrika_verified: true,
83
+ mudrika_ttl_remaining_s: 600,
84
+ pramana_chain_depth: 2,
85
+ }),
86
+ scoreAxis({
87
+ axis: 'identity_broadcast',
88
+ self_declared: true,
89
+ declared_fields: ['agentId', 'agentType', 'officerRole', 'scopeKey', 'taskId', 'delegatedBy'],
90
+ }),
91
+ scoreAxis({
92
+ axis: 'mandate_bounds',
93
+ trust_mask_granted: 0b11111,
94
+ trust_mask_requested: 0b01111,
95
+ scope_key_match: true,
96
+ ttl_respected: true,
97
+ }),
98
+ scoreAxis({
99
+ axis: 'proportional_force',
100
+ response_mode: 1,
101
+ }),
102
+ scoreAxis({
103
+ axis: 'return_with_proof',
104
+ receipt_filed: true,
105
+ receipt_signed: true,
106
+ actions_listed: true,
107
+ deviations_reported: true,
108
+ }),
109
+ scoreAxis({
110
+ axis: 'no_overreach',
111
+ trust_mask_granted: 0b11111,
112
+ trust_mask_used: 0b00111,
113
+ }),
114
+ scoreAxis({
115
+ axis: 'truthful_report',
116
+ before_state_present: true,
117
+ after_state_present: true,
118
+ errors_reported: true,
119
+ human_modified_flagged: true,
120
+ }),
121
+ ];
122
+
123
+ const posture = computePostureScore(axisScores);
124
+ // {
125
+ // overall_score: 100,
126
+ // overall_grade: 'A',
127
+ // violation_count: 0,
128
+ // warn_count: 0,
129
+ // axes: { mudrika_integrity: {...}, identity_broadcast: {...}, ... }
130
+ // }
131
+ ```
132
+
133
+ ### The 7 axes
134
+
135
+ | Axis | Rule | What it checks |
136
+ |---|---|---|
137
+ | `mudrika_integrity` | HNG-S-001 | Credential present + verified + non-expiring |
138
+ | `identity_broadcast` | HNG-S-002 | Agent self-declared with required fields |
139
+ | `mandate_bounds` | HNG-S-003 | Requested mask ≤ granted mask, scope match, TTL respected |
140
+ | `proportional_force` | HNG-S-004 | Response mode (1/2/3) properly routed |
141
+ | `return_with_proof` | HNG-S-005 | Task closed with signed receipt + actions list |
142
+ | `no_overreach` | HNG-S-006 | Used bits ⊆ granted bits + utilisation signal |
143
+ | `truthful_report` | HNG-S-007 | before/after state present, errors + human-modification declared |
144
+
145
+ ### Grade thresholds
146
+
147
+ | Grade | Condition |
148
+ |---|---|
149
+ | A | overall_score ≥ 90, no violations |
150
+ | B | overall_score ≥ 80, no violations |
151
+ | C | overall_score ≥ 60, no violations |
152
+ | D | overall_score < 60, OR 1–2 violations |
153
+ | F | 3+ violations |
154
+
155
+ ## What this package does NOT do
156
+
157
+ - **No DB.** No persistence. The full xshieldai-hanumang service stores attestations to SQLite — you'd need to write your own store on top of these primitives.
158
+ - **No HTTP.** `revocation_url` is in the mudrika payload but `verifyMudrika()` does not call it. The full service does (HNG-S-011, EE feature).
159
+ - **No signature crypto.** See Phase-1 limit above.
160
+ - **No regression alerting.** The full service tracks posture over time and routes alerts; not here.
161
+ - **No registry / fleet view.** That's the EE layer.
162
+
163
+ ## Honest discipline
164
+
165
+ `hanumang-mandate` was extracted from a service that runs internally at `trust_mask=1`, `claude_ankr_mask=29`, `claw_mask=14255`. Those scores describe the **full service**, not this primitives-only SDK. The package itself is v0.1.0 — a first OSS surface of the credential + scoring primitives, audited in extraction but not independently CA-audited as a standalone artifact.
166
+
167
+ The Phase-1 signature limit is real. Use this for structural verification in trusted-transport environments, not for crypto-attested mandates over untrusted channels.
168
+
169
+ ## Related
170
+
171
+ - [`@rocketlang/aegis`](https://www.npmjs.com/package/@rocketlang/aegis) — spawn-time HanumanG + DAN gate + budget caps (complementary to this)
172
+ - [`@rocketlang/kavachos`](https://www.npmjs.com/package/@rocketlang/kavachos) — seccomp-bpf + Falco behavior governance
173
+ - [`@rocketlang/chitta-detect`](https://www.npmjs.com/package/@rocketlang/chitta-detect) — memory poisoning detection
174
+ - [`@rocketlang/lakshmanrekha`](https://www.npmjs.com/package/@rocketlang/lakshmanrekha) — LLM endpoint probe suite
175
+ - [`@rocketlang/aegis-guard`](https://www.npmjs.com/package/@rocketlang/aegis-guard) — Five Locks SDK
176
+
177
+ ## License
178
+
179
+ AGPL-3.0-only. See [LICENSE](LICENSE). Any modified version run as a network service must publish source per AGPL clause 13.
180
+
181
+ The full xshieldai-hanumang service is internal (port 4255) and not currently distributed.
182
+
183
+ For commercial dual-licensing or partnership: [captain@ankr.in](mailto:captain@ankr.in).
184
+
185
+ ---
186
+
187
+ ## v0.2.0 — Opt-in Agentic Control Center (ACC) event bus
188
+
189
+ Added 2026-05-17. `verifyMudrika()`, `scoreAxis()`, and
190
+ `computePostureScore()` now emit `AccReceipt` events, **but only when
191
+ you wire a bus**. Without `setEventBus`, v0.2.0 behaves identically to
192
+ v0.1.0 — no emission, no state, no side effect.
193
+
194
+ ### Wire it in 3 lines
195
+
196
+ ```typescript
197
+ import { setEventBus, type EventBus, type AccReceipt } from '@rocketlang/hanumang-mandate';
198
+
199
+ const myBus: EventBus = {
200
+ emit: (r: AccReceipt) => console.log(`[ACC] ${r.event_type} ${r.verdict} ${r.summary}`),
201
+ };
202
+ setEventBus(myBus);
203
+ ```
204
+
205
+ ### Receipt events emitted
206
+
207
+ | Primitive | event_type | verdict |
208
+ |---|---|---|
209
+ | `verifyMudrika` (PASS) | `mudrika.verified` | PASS |
210
+ | `verifyMudrika` (FAIL / EXPIRED) | `mudrika.rejected` | FAIL |
211
+ | `scoreAxis` (per axis) | `posture.axis_scored` | PASS / WARN / FAIL |
212
+ | `computePostureScore` (aggregate) | `posture.scored` | `{grade}-{verdict}` (e.g. `A-PASS`, `D-FAIL`) |
213
+
214
+ ### Receipt shape
215
+
216
+ ```typescript
217
+ interface AccReceipt {
218
+ receipt_id: string; // primitive-prefixed: 'hanumang-mudrika-{mudrikaId}' etc.
219
+ primitive: string; // always 'hanumang-mandate'
220
+ event_type: string; // 'mudrika.verified' | 'mudrika.rejected' | 'posture.axis_scored' | 'posture.scored'
221
+ emitted_at: string; // ISO 8601
222
+ agent_id?: string; // populated from mudrika.agent_id on verifyMudrika; not yet on scoreAxis
223
+ verdict?: string;
224
+ rules_fired?: string[]; // HNG-* rule IDs
225
+ summary?: string;
226
+ payload?: Record<string, unknown>;
227
+ }
228
+ ```
229
+
230
+ Strict subset of EE PRAMANA receipt format — EE consumers ingest without translation.
231
+
232
+ ### Phase-1 limits (v0.2.0)
233
+
234
+ - **agent_id is populated on `mudrika.verified` only** — scoreAxis and
235
+ computePostureScore don't currently receive an agent_id parameter. Future
236
+ versions may add an optional `agent_id` field to `AxisInput`; today,
237
+ post-process receipts in the bus to add agent context from your own tracking.
238
+ - **scoreAxis emits per call** — if you score 7 axes for one agent, that's
239
+ 7 `posture.axis_scored` events + 1 `posture.scored` aggregate = 8 receipts.
240
+ Cockpit consumers may want to collapse these for display.
241
+ - **Mudrika signature crypto NOT verified** (unchanged from v0.1.0) —
242
+ emission says `mudrika.verified` based on structural + TTL + trust_mask
243
+ range checks only. Phase-2 will add cryptographic signature verification.
244
+ - **Default bus is in-process only.** Multi-process buses are a consumer choice.
245
+
246
+ ### Use with `@rocketlang/aegis-suite`
247
+
248
+ ```typescript
249
+ import { wireAllToBus } from '@rocketlang/aegis-suite'; // suite v0.2.0+
250
+ wireAllToBus(); // wires aegis-guard + chitta-detect + lakshmanrekha + hanumang-mandate at once
251
+ ```
package/package.json ADDED
@@ -0,0 +1,81 @@
1
+ {
2
+ "name": "@xshieldai/hanumang-mandate",
3
+ "version": "0.2.0",
4
+ "description": "HanumanG mandate primitives — Mudrika delegation credential verifier + 7-axis posture scorer (mudrika_integrity, identity_broadcast, mandate_bounds, proportional_force, return_with_proof, no_overreach, truthful_report) + opt-in Agentic Control Center event bus. Extracted from xshieldai-hanumang.",
5
+ "license": "AGPL-3.0-only",
6
+ "type": "module",
7
+ "author": "Capt. Anil Sharma <capt.anil.sharma@powerpbox.org>",
8
+ "homepage": "https://github.com/rocketlang/aegis/tree/main/packages/hanumang-mandate",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/rocketlang/aegis.git",
12
+ "directory": "packages/hanumang-mandate"
13
+ },
14
+ "bugs": {
15
+ "url": "https://github.com/rocketlang/aegis/issues"
16
+ },
17
+ "keywords": [
18
+ "hanumang",
19
+ "mudrika",
20
+ "xshieldai",
21
+ "rocketlang",
22
+ "ai-agent-delegation",
23
+ "agent-governance",
24
+ "ai-attestation",
25
+ "posture-scoring",
26
+ "agent-safety",
27
+ "delegation-credential",
28
+ "ai-mandate"
29
+ ],
30
+ "exports": {
31
+ ".": {
32
+ "import": "./src/index.ts",
33
+ "types": "./src/index.ts"
34
+ }
35
+ },
36
+ "main": "./src/index.ts",
37
+ "files": [
38
+ "src/",
39
+ "README.md",
40
+ "LICENSE"
41
+ ],
42
+ "scripts": {
43
+ "typecheck": "tsc --noEmit"
44
+ },
45
+ "devDependencies": {
46
+ "typescript": "^5.4.0",
47
+ "@types/node": "^20.0.0"
48
+ },
49
+ "engines": {
50
+ "bun": ">=1.0.0",
51
+ "node": ">=18.0.0"
52
+ },
53
+ "publishConfig": {
54
+ "access": "public"
55
+ },
56
+ "hanumang_mandate": {
57
+ "extracted_from": "xshieldai-hanumang (internal Fastify service, port 4255)",
58
+ "complementary_to": "@rocketlang/aegis HanumanG (spawn-time governance)",
59
+ "phase": "phase-1-complete (structural mudrika verification; signature crypto deferred to phase-2)",
60
+ "rules_implemented": [
61
+ "HNG-S-001 (mudrika integrity axis)",
62
+ "HNG-S-002 (identity broadcast axis)",
63
+ "HNG-S-003 (mandate bounds axis)",
64
+ "HNG-S-004 (proportional force axis)",
65
+ "HNG-S-005 (return with proof axis)",
66
+ "HNG-S-006 (no overreach axis)",
67
+ "HNG-S-007 (truthful report axis)",
68
+ "HNG-S-008 (mudrika structure)",
69
+ "HNG-S-009 (mudrika TTL check)",
70
+ "HNG-S-010 (trust_mask range)",
71
+ "HNG-YK-001 (worst-axis floor aggregate grade)"
72
+ ],
73
+ "rules_left_in_ee": [
74
+ "HNG-S-011 (revocation URL reachability — requires HTTP)",
75
+ "Signature crypto verification (Phase-2)",
76
+ "Attestation persistence (SQLite-backed)",
77
+ "Regression alert routing (partial in EE)",
78
+ "Forja STATE/TRUST/SENSE/PROOF endpoints"
79
+ ]
80
+ }
81
+ }
package/src/acc-bus.ts ADDED
@@ -0,0 +1,48 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-only
2
+ // Copyright (c) 2026 Capt. Anil Sharma (rocketlang). All rights reserved.
3
+ // See LICENSE for details.
4
+ //
5
+ // @rocketlang/hanumang-mandate — opt-in Agentic Control Center event bus (v0.2.0)
6
+ // @rule:ACC-003 — Opt-in. emit only when setEventBus() called.
7
+ // @rule:ACC-004 — Lightweight OSS receipt shape (strict subset of EE PRAMANA).
8
+ // @rule:ACC-YK-003 — Stateless-primitive contract preserved.
9
+ // @rule:INF-ACC-005 — emit() is a no-op when no bus has been set.
10
+
11
+ export interface AccReceipt {
12
+ receipt_id: string;
13
+ primitive: string;
14
+ event_type: string;
15
+ emitted_at: string;
16
+ agent_id?: string;
17
+ verdict?: string;
18
+ rules_fired?: string[];
19
+ summary?: string;
20
+ payload?: Record<string, unknown>;
21
+ }
22
+
23
+ export interface EventBus {
24
+ emit(receipt: AccReceipt): void;
25
+ }
26
+
27
+ let _bus: EventBus | null = null;
28
+
29
+ export function setEventBus(bus: EventBus | null): void {
30
+ _bus = bus;
31
+ }
32
+
33
+ export function emitAccReceipt(receipt: Omit<AccReceipt, 'primitive' | 'emitted_at'>): void {
34
+ if (!_bus) return;
35
+ try {
36
+ _bus.emit({
37
+ ...receipt,
38
+ primitive: 'hanumang-mandate',
39
+ emitted_at: new Date().toISOString(),
40
+ });
41
+ } catch {
42
+ // bus implementation failure must never break the primitive's caller
43
+ }
44
+ }
45
+
46
+ export function isBusWired(): boolean {
47
+ return _bus !== null;
48
+ }
package/src/index.ts ADDED
@@ -0,0 +1,51 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-only
2
+ // Copyright (c) 2026 Capt. Anil Sharma (rocketlang). All rights reserved.
3
+ //
4
+ // @rocketlang/hanumang-mandate — Agent delegation credential + 7-axis posture scoring.
5
+ //
6
+ // Extracted from xshieldai-hanumang (the full Fastify service with SQLite-
7
+ // backed attestations, Forja STATE/TRUST/SENSE/PROOF endpoints, and
8
+ // Phase-2 regression alert routing). This package contains ONLY the
9
+ // pure primitives — Mudrika credential verification + 7-axis scorer.
10
+ //
11
+ // COMPLEMENTARY to @rocketlang/aegis HanumanG (spawn-time governance):
12
+ // @rocketlang/aegis → "Can this agent SPAWN?" (PreToolUse gate)
13
+ // @rocketlang/hanumang-mandate → "Is this agent's MANDATE valid?" + posture
14
+ //
15
+ // Public surface:
16
+ // import {
17
+ // verifyMudrika, scoreAxis, computePostureScore,
18
+ // } from '@rocketlang/hanumang-mandate';
19
+
20
+ export {
21
+ verifyMudrika,
22
+ } from './mudrika.js';
23
+
24
+ export type {
25
+ MudrikaPayload,
26
+ VerifyOutcome,
27
+ VerifyResult,
28
+ } from './mudrika.js';
29
+
30
+ export {
31
+ scoreAxis,
32
+ computePostureScore,
33
+ } from './scorer.js';
34
+
35
+ export type {
36
+ Axis,
37
+ AxisOutcome,
38
+ AxisInput,
39
+ AxisScore,
40
+ PostureScore,
41
+ } from './scorer.js';
42
+
43
+ // @rule:ACC-003 — Opt-in event bus for Agentic Control Center observability.
44
+ // Stateless contract preserved (ACC-YK-003): emit is no-op
45
+ // when setEventBus has not been called. v0.2.0+.
46
+ export {
47
+ type AccReceipt,
48
+ type EventBus,
49
+ setEventBus,
50
+ isBusWired,
51
+ } from './acc-bus.js';
package/src/mudrika.ts ADDED
@@ -0,0 +1,167 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-only
2
+ // Copyright (c) 2026 Capt. Anil Sharma (rocketlang). All rights reserved.
3
+ // See LICENSE for details.
4
+
5
+ // HanumanG — Mudrika verification engine
6
+ // @rule:HNG-S-001 — mudrika is the mandatory delegation credential; no mudrika = refuse
7
+ // @rule:HNG-S-008 — mudrika structure: principal_id, agent_id, trust_mask, scope_key, ttl, pramana_chain
8
+ // @rule:HNG-S-009 — mudrika TTL must not be expired at verification time
9
+ // @rule:HNG-S-010 — trust_mask in mudrika must be ≤ trust_mask of principal (spawn invariant)
10
+ // @rule:HNG-S-011 — revocation_url must be reachable; REVOKED mudrikas refuse immediately
11
+ //
12
+ // PHASE-1 LIMIT: verifyMudrika() validates STRUCTURE + TTL + trust_mask range only.
13
+ // It does NOT cryptographically verify the `signature` field. Phase-2 will add
14
+ // signature verification. Today, mudrika trust is assumed to come from an
15
+ // authenticated channel; callers must verify provenance themselves.
16
+
17
+ import { emitAccReceipt } from './acc-bus.js';
18
+
19
+ export interface MudrikaPayload {
20
+ mudrika_version: string;
21
+ mudrika_id: string;
22
+ principal_id: string;
23
+ agent_id: string;
24
+ task_id: string;
25
+ trust_mask: number;
26
+ scope_key: string;
27
+ issued_at: string;
28
+ ttl_seconds: number;
29
+ required_return_proof: string;
30
+ revocation_url: string;
31
+ pramana_chain: string[];
32
+ signature?: string;
33
+ }
34
+
35
+ export type VerifyOutcome = 'PASS' | 'FAIL' | 'EXPIRED' | 'REVOKED';
36
+
37
+ export interface VerifyResult {
38
+ outcome: VerifyOutcome;
39
+ failure_reason: string | null;
40
+ expires_at: string;
41
+ trust_mask: number;
42
+ scope_key: string;
43
+ principal_id: string;
44
+ mudrika_id: string;
45
+ pramana_chain: string[];
46
+ duration_ms: number;
47
+ }
48
+
49
+ export function verifyMudrika(raw: unknown, expected_agent_id?: string): VerifyResult {
50
+ const t0 = Date.now();
51
+
52
+ if (!raw || typeof raw !== 'object') {
53
+ return fail('mudrika_missing', t0);
54
+ }
55
+
56
+ const m = raw as Partial<MudrikaPayload>;
57
+
58
+ // Required fields
59
+ if (
60
+ !m.mudrika_id ||
61
+ !m.principal_id ||
62
+ !m.agent_id ||
63
+ !m.task_id ||
64
+ !m.scope_key ||
65
+ !m.issued_at ||
66
+ !m.ttl_seconds
67
+ ) {
68
+ return fail('missing_required_fields', t0);
69
+ }
70
+
71
+ // Agent ID must match if provided
72
+ if (expected_agent_id && m.agent_id !== expected_agent_id) {
73
+ return fail(`agent_id_mismatch: expected ${expected_agent_id} got ${m.agent_id}`, t0);
74
+ }
75
+
76
+ // TTL check — @rule:HNG-S-009
77
+ const issuedAt = new Date(m.issued_at).getTime();
78
+ if (isNaN(issuedAt)) return fail('invalid_issued_at', t0);
79
+ const expiresAt = new Date(issuedAt + (m.ttl_seconds ?? 0) * 1000);
80
+ if (Date.now() > expiresAt.getTime()) {
81
+ const expiredResult: VerifyResult = {
82
+ outcome: 'EXPIRED',
83
+ failure_reason: `mudrika expired at ${expiresAt.toISOString()}`,
84
+ expires_at: expiresAt.toISOString(),
85
+ trust_mask: m.trust_mask ?? 0,
86
+ scope_key: m.scope_key ?? '',
87
+ principal_id: m.principal_id ?? '',
88
+ mudrika_id: m.mudrika_id ?? '',
89
+ pramana_chain: m.pramana_chain ?? [],
90
+ duration_ms: Date.now() - t0,
91
+ };
92
+ emitAccReceipt({
93
+ receipt_id: `hanumang-mudrika-expired-${m.mudrika_id ?? t0}`,
94
+ event_type: 'mudrika.rejected',
95
+ agent_id: m.agent_id,
96
+ verdict: 'EXPIRED',
97
+ rules_fired: ['HNG-S-009'],
98
+ summary: `mudrika ${m.mudrika_id ?? '?'} expired at ${expiresAt.toISOString()}`,
99
+ });
100
+ return expiredResult;
101
+ }
102
+
103
+ // Spawn invariant — child trust_mask ≤ declared maximum (32-bit)
104
+ // @rule:HNG-S-010 + BitMask OS spawn invariant
105
+ const trust_mask = m.trust_mask ?? 0;
106
+ if (trust_mask < 0 || trust_mask > 0xffffffff) {
107
+ return fail('trust_mask_out_of_range', t0);
108
+ }
109
+
110
+ // Pramana chain present (warning if empty — not blocking at verification stage)
111
+ const pramana_chain = m.pramana_chain ?? [];
112
+
113
+ const result: VerifyResult = {
114
+ outcome: 'PASS',
115
+ failure_reason: null,
116
+ expires_at: expiresAt.toISOString(),
117
+ trust_mask,
118
+ scope_key: m.scope_key,
119
+ principal_id: m.principal_id,
120
+ mudrika_id: m.mudrika_id,
121
+ pramana_chain,
122
+ duration_ms: Date.now() - t0,
123
+ };
124
+
125
+ // @rule:ACC-003 @rule:ACC-004 — emit ACC receipt for cockpit observability
126
+ emitAccReceipt({
127
+ receipt_id: `hanumang-mudrika-${m.mudrika_id}`,
128
+ event_type: 'mudrika.verified',
129
+ agent_id: m.agent_id,
130
+ verdict: 'PASS',
131
+ rules_fired: ['HNG-S-008', 'HNG-S-009', 'HNG-S-010'],
132
+ summary: `mudrika ${m.mudrika_id} verified for ${m.principal_id} → ${m.agent_id} scope=${m.scope_key}`,
133
+ payload: {
134
+ trust_mask,
135
+ expires_at: result.expires_at,
136
+ pramana_chain_depth: pramana_chain.length,
137
+ duration_ms: result.duration_ms,
138
+ },
139
+ });
140
+
141
+ return result;
142
+ }
143
+
144
+ function fail(reason: string, t0: number): VerifyResult {
145
+ const result: VerifyResult = {
146
+ outcome: 'FAIL',
147
+ failure_reason: reason,
148
+ expires_at: new Date().toISOString(),
149
+ trust_mask: 0,
150
+ scope_key: '',
151
+ principal_id: '',
152
+ mudrika_id: '',
153
+ pramana_chain: [],
154
+ duration_ms: Date.now() - t0,
155
+ };
156
+
157
+ // @rule:ACC-003 — emit failure receipt for cockpit observability
158
+ emitAccReceipt({
159
+ receipt_id: `hanumang-mudrika-fail-${t0}`,
160
+ event_type: 'mudrika.rejected',
161
+ verdict: 'FAIL',
162
+ rules_fired: ['HNG-S-008', 'HNG-S-009', 'HNG-S-010'],
163
+ summary: `mudrika rejected: ${reason}`,
164
+ });
165
+
166
+ return result;
167
+ }
package/src/scorer.ts ADDED
@@ -0,0 +1,302 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-only
2
+ // Copyright (c) 2026 Capt. Anil Sharma (rocketlang). All rights reserved.
3
+ // See LICENSE for details.
4
+
5
+ // HanumanG — 7-axis posture scorer
6
+ // @rule:HNG-S-001 axis 1: mudrika integrity
7
+ // @rule:HNG-S-002 axis 2: identity broadcast
8
+ // @rule:HNG-S-003 axis 3: mandate bounds
9
+ // @rule:HNG-S-004 axis 4: proportional force
10
+ // @rule:HNG-S-005 axis 5: return with proof
11
+ // @rule:HNG-S-006 axis 6: no overreach
12
+ // @rule:HNG-S-007 axis 7: truthful report
13
+ // @rule:HNG-YK-001 — aggregate grade uses worst-axis floor, not average
14
+
15
+ import { emitAccReceipt } from './acc-bus.js';
16
+
17
+ export type Axis =
18
+ | 'mudrika_integrity'
19
+ | 'identity_broadcast'
20
+ | 'mandate_bounds'
21
+ | 'proportional_force'
22
+ | 'return_with_proof'
23
+ | 'no_overreach'
24
+ | 'truthful_report';
25
+
26
+ export type AxisOutcome = 'PASS' | 'WARN' | 'FAIL';
27
+
28
+ export interface AxisInput {
29
+ axis: Axis;
30
+ // Axis 1: mudrika integrity
31
+ mudrika_verified?: boolean;
32
+ mudrika_ttl_remaining_s?: number;
33
+ pramana_chain_depth?: number;
34
+ // Axis 2: identity broadcast
35
+ self_declared?: boolean;
36
+ declared_fields?: string[];
37
+ // Axis 3: mandate bounds
38
+ trust_mask_granted?: number;
39
+ trust_mask_requested?: number;
40
+ scope_key_match?: boolean;
41
+ ttl_respected?: boolean;
42
+ // Axis 4: proportional force
43
+ response_mode?: 1 | 2 | 3;
44
+ standing_order_exists?: boolean;
45
+ // Axis 5: return with proof
46
+ receipt_filed?: boolean;
47
+ receipt_signed?: boolean;
48
+ actions_listed?: boolean;
49
+ deviations_reported?: boolean;
50
+ // Axis 6: no overreach
51
+ trust_mask_used?: number;
52
+ // Axis 7: truthful report
53
+ before_state_present?: boolean;
54
+ after_state_present?: boolean;
55
+ errors_reported?: boolean;
56
+ human_modified_flagged?: boolean;
57
+ // shared
58
+ task_id?: string;
59
+ evidence?: string;
60
+ }
61
+
62
+ export interface AxisScore {
63
+ axis: Axis;
64
+ score: number;
65
+ outcome: AxisOutcome;
66
+ rule_id: string;
67
+ notes: string[];
68
+ }
69
+
70
+ export interface PostureScore {
71
+ overall_score: number;
72
+ overall_grade: 'A' | 'B' | 'C' | 'D' | 'F';
73
+ axes: Record<Axis, AxisScore>;
74
+ violation_count: number;
75
+ warn_count: number;
76
+ }
77
+
78
+ const AXIS_RULES: Record<Axis, string> = {
79
+ mudrika_integrity: 'HNG-S-001',
80
+ identity_broadcast: 'HNG-S-002',
81
+ mandate_bounds: 'HNG-S-003',
82
+ proportional_force: 'HNG-S-004',
83
+ return_with_proof: 'HNG-S-005',
84
+ no_overreach: 'HNG-S-006',
85
+ truthful_report: 'HNG-S-007',
86
+ };
87
+
88
+ export function scoreAxis(input: AxisInput): AxisScore {
89
+ const notes: string[] = [];
90
+ let score = 100;
91
+
92
+ switch (input.axis) {
93
+ case 'mudrika_integrity': {
94
+ // @rule:HNG-S-001 — no mudrika = FAIL immediately
95
+ if (!input.mudrika_verified) {
96
+ score = 0;
97
+ notes.push('mudrika absent or failed verification');
98
+ break;
99
+ }
100
+ if ((input.mudrika_ttl_remaining_s ?? 60) < 30) {
101
+ score -= 20;
102
+ notes.push('mudrika expires in <30s');
103
+ }
104
+ if ((input.pramana_chain_depth ?? 0) === 0) {
105
+ score -= 10;
106
+ notes.push('empty pramana chain');
107
+ }
108
+ break;
109
+ }
110
+
111
+ case 'identity_broadcast': {
112
+ // @rule:HNG-S-002 — no self-declaration = FAIL
113
+ if (!input.self_declared) {
114
+ score = 0;
115
+ notes.push('agent did not self-declare before action');
116
+ break;
117
+ }
118
+ const required = ['agentId', 'agentType', 'officerRole', 'scopeKey', 'taskId', 'delegatedBy'];
119
+ const declared = input.declared_fields ?? [];
120
+ const missing = required.filter((f) => !declared.includes(f));
121
+ if (missing.length > 0) {
122
+ score -= missing.length * 10;
123
+ notes.push(`missing declaration fields: ${missing.join(', ')}`);
124
+ }
125
+ break;
126
+ }
127
+
128
+ case 'mandate_bounds': {
129
+ // @rule:HNG-S-003 — exceeding any bound = immediate FAIL
130
+ if (input.trust_mask_requested !== undefined && input.trust_mask_granted !== undefined) {
131
+ // spawn invariant: child cannot request bits parent doesn't have
132
+ if ((input.trust_mask_requested & ~input.trust_mask_granted) !== 0) {
133
+ score = 0;
134
+ notes.push(
135
+ `trust_mask_requested(${input.trust_mask_requested}) exceeds trust_mask_granted(${input.trust_mask_granted})`
136
+ );
137
+ break;
138
+ }
139
+ }
140
+ if (input.scope_key_match === false) {
141
+ score = 0;
142
+ notes.push('action outside declared scope_key');
143
+ break;
144
+ }
145
+ if (input.ttl_respected === false) {
146
+ score -= 40;
147
+ notes.push('action attempted after TTL expiry');
148
+ }
149
+ break;
150
+ }
151
+
152
+ case 'proportional_force': {
153
+ // @rule:HNG-S-004 — mode must be correctly routed
154
+ const mode = input.response_mode ?? 1;
155
+ if (mode === 2 && !input.standing_order_exists) {
156
+ score = 0;
157
+ notes.push('mode-2 execution without prior standing order');
158
+ break;
159
+ }
160
+ if (mode === 3) {
161
+ notes.push('mode-3 existential action: always permitted');
162
+ }
163
+ break;
164
+ }
165
+
166
+ case 'return_with_proof': {
167
+ // @rule:HNG-S-005 — incomplete receipt = FAIL
168
+ if (!input.receipt_filed) {
169
+ score = 0;
170
+ notes.push('task closed without return receipt');
171
+ break;
172
+ }
173
+ if (!input.receipt_signed) {
174
+ score -= 30;
175
+ notes.push('receipt unsigned (SAKSHI countersign missing)');
176
+ }
177
+ if (!input.actions_listed) {
178
+ score -= 30;
179
+ notes.push('actions_taken list absent in receipt');
180
+ }
181
+ if (!input.deviations_reported && input.deviations_reported !== undefined) {
182
+ score -= 20;
183
+ notes.push('deviations not reported');
184
+ }
185
+ break;
186
+ }
187
+
188
+ case 'no_overreach': {
189
+ // @rule:HNG-S-006 — used bits vs granted bits
190
+ const granted = input.trust_mask_granted ?? 0;
191
+ const used = input.trust_mask_used ?? 0;
192
+ if (granted === 0) break;
193
+ // Bits used that were not granted = overreach
194
+ if ((used & ~granted) !== 0) {
195
+ score = 0;
196
+ notes.push(`overreach: used bits ${used & ~granted} not in granted mask`);
197
+ break;
198
+ }
199
+ const grantedBits = popcount(granted);
200
+ const usedBits = popcount(used);
201
+ const utilisation = grantedBits > 0 ? usedBits / grantedBits : 0;
202
+ if (utilisation > 0.8) {
203
+ score -= 20;
204
+ notes.push(
205
+ `high utilisation ${Math.round(utilisation * 100)}%: over-provisioning signal (HNG-S-006)`
206
+ );
207
+ }
208
+ break;
209
+ }
210
+
211
+ case 'truthful_report': {
212
+ // @rule:HNG-S-007 — omission = violation
213
+ if (!input.before_state_present) {
214
+ score -= 30;
215
+ notes.push('before_state absent (CA-003 / HNG-S-007)');
216
+ }
217
+ if (!input.after_state_present) {
218
+ score -= 30;
219
+ notes.push('after_state absent (CA-003 / HNG-S-007)');
220
+ }
221
+ if (input.errors_reported === false) {
222
+ score -= 20;
223
+ notes.push('errors not reported (truthful report violated)');
224
+ }
225
+ if (input.human_modified_flagged === false) {
226
+ score -= 10;
227
+ notes.push('human_modified not declared (CA-005)');
228
+ }
229
+ break;
230
+ }
231
+ }
232
+
233
+ score = Math.max(0, Math.min(100, score));
234
+ const outcome: AxisOutcome = score >= 80 ? 'PASS' : score >= 50 ? 'WARN' : 'FAIL';
235
+ const result: AxisScore = { axis: input.axis, score, outcome, rule_id: AXIS_RULES[input.axis], notes };
236
+
237
+ // @rule:ACC-003 — emit per-axis score (no-op when bus unset)
238
+ emitAccReceipt({
239
+ receipt_id: `hanumang-axis-${input.axis}-${input.task_id ?? 'unspec'}-${Date.now()}`,
240
+ event_type: 'posture.axis_scored',
241
+ agent_id: undefined,
242
+ verdict: outcome,
243
+ rules_fired: [result.rule_id],
244
+ summary: `axis=${input.axis} score=${score} outcome=${outcome} task=${input.task_id ?? 'unspec'}`,
245
+ payload: { axis: input.axis, score, notes: notes.slice(0, 5) },
246
+ });
247
+
248
+ return result;
249
+ }
250
+
251
+ export function computePostureScore(axisScores: AxisScore[]): PostureScore {
252
+ // @rule:HNG-YK-001 — worst-axis floor: a single FAIL caps the grade at D
253
+ const scores = axisScores.map((a) => a.score);
254
+ const overall_score =
255
+ scores.length > 0 ? Math.round(scores.reduce((s, n) => s + n, 0) / scores.length) : 0;
256
+ const violation_count = axisScores.filter((a) => a.outcome === 'FAIL').length;
257
+ const warn_count = axisScores.filter((a) => a.outcome === 'WARN').length;
258
+
259
+ let overall_grade: 'A' | 'B' | 'C' | 'D' | 'F';
260
+ if (violation_count > 0) {
261
+ overall_grade = violation_count >= 3 ? 'F' : 'D';
262
+ } else if (overall_score >= 90) {
263
+ overall_grade = 'A';
264
+ } else if (overall_score >= 80) {
265
+ overall_grade = 'B';
266
+ } else if (overall_score >= 60) {
267
+ overall_grade = 'C';
268
+ } else {
269
+ overall_grade = 'D';
270
+ }
271
+
272
+ const axes: Record<Axis, AxisScore> = {} as Record<Axis, AxisScore>;
273
+ for (const a of axisScores) axes[a.axis as Axis] = a;
274
+
275
+ const result: PostureScore = { overall_score, overall_grade, axes, violation_count, warn_count };
276
+
277
+ // @rule:ACC-003 @rule:HNG-YK-001 — emit aggregate posture (worst-axis floor)
278
+ const verdict =
279
+ overall_grade === 'A' || overall_grade === 'B' ? 'PASS'
280
+ : overall_grade === 'C' ? 'WARN'
281
+ : 'FAIL';
282
+ emitAccReceipt({
283
+ receipt_id: `hanumang-posture-${Date.now()}`,
284
+ event_type: 'posture.scored',
285
+ verdict: `${overall_grade}-${verdict}`,
286
+ rules_fired: ['HNG-YK-001'],
287
+ summary: `posture grade=${overall_grade} score=${overall_score}/100 violations=${violation_count} warns=${warn_count}`,
288
+ payload: { overall_score, overall_grade, violation_count, warn_count, axes_evaluated: axisScores.length },
289
+ });
290
+
291
+ return result;
292
+ }
293
+
294
+ function popcount(n: number): number {
295
+ let count = 0;
296
+ let x = n >>> 0;
297
+ while (x) {
298
+ count += x & 1;
299
+ x >>>= 1;
300
+ }
301
+ return count;
302
+ }