@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 +31 -0
- package/README.md +251 -0
- package/package.json +81 -0
- package/src/acc-bus.ts +48 -0
- package/src/index.ts +51 -0
- package/src/mudrika.ts +167 -0
- package/src/scorer.ts +302 -0
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
|
+
}
|