@rocketlang/aegis-guard 0.1.0 → 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/README.md CHANGED
@@ -127,3 +127,89 @@ const redisNonceStore: NonceStore = {
127
127
  ## License
128
128
 
129
129
  AGPL-3.0 — Capt. Anil Sharma, powerpbox.org
130
+
131
+ ---
132
+
133
+ ## v0.2.0 — Opt-in Agentic Control Center (ACC) event bus
134
+
135
+ Added 2026-05-17. Each Five Locks primitive now emits an `AccReceipt` on
136
+ success or failure, **but only when you wire a bus**. Without `setEventBus`,
137
+ v0.2.0 behaves identically to v0.1.0 — no emission, no state, no side effect.
138
+
139
+ ### Wire it in 3 lines
140
+
141
+ ```typescript
142
+ import { setEventBus, type EventBus, type AccReceipt } from '@rocketlang/aegis-guard';
143
+
144
+ const myBus: EventBus = {
145
+ emit: (r: AccReceipt) => console.log(`[ACC] ${r.event_type} verdict=${r.verdict} ${r.summary}`),
146
+ };
147
+ setEventBus(myBus);
148
+ ```
149
+
150
+ Now every primitive call emits a receipt. Pass `null` to `setEventBus` to detach.
151
+
152
+ ### Receipt events emitted
153
+
154
+ | Primitive | event_type on success | event_type on failure |
155
+ |---|---|---|
156
+ | `verifyApprovalToken` | `lock.approval.verified` (PASS) | `lock.approval.rejected` (FAIL) |
157
+ | `verifyAndConsumeNonce` | `lock.nonce.consumed` (PASS) | `lock.nonce.rejected` (FAIL) |
158
+ | `checkIdempotency` | `lock.idempotency.duplicate` (PASS) OR `lock.idempotency.mismatch` (WARN) | (no event for non-duplicate path) |
159
+ | `emitAegisSenseEvent` | `lock.sense.emitted` (PASS or WARN if irreversible) | — |
160
+
161
+ ### Receipt shape
162
+
163
+ ```typescript
164
+ interface AccReceipt {
165
+ receipt_id: string; // primitive-prefixed identifier
166
+ primitive: string; // always 'aegis-guard' for this package
167
+ event_type: string; // lock.*
168
+ emitted_at: string; // ISO 8601
169
+ agent_id?: string; // reserved — not yet populated by aegis-guard
170
+ verdict?: string; // PASS | FAIL | WARN
171
+ rules_fired?: string[]; // e.g. ['AEG-E-016']
172
+ summary?: string; // ≤200 chars
173
+ payload?: Record<string, unknown>;
174
+ }
175
+ ```
176
+
177
+ The shape is a strict subset of the EE PRAMANA receipt format. EE
178
+ consumers ingest these events without translation.
179
+
180
+ ### Phase-1 limits (v0.2.0)
181
+
182
+ - **agent_id is not yet populated** — primitives don't receive an agent
183
+ context as parameter. Future versions may add an optional `agent_id`
184
+ argument to each primitive; today you can post-process receipts in the
185
+ bus to add agent context from your own tracking.
186
+ - **`buildIdempotencyFingerprint`, `digestApprovalToken`, `mintApprovalToken`
187
+ do NOT emit** — they're pure helpers called many times per operation.
188
+ Emitting from them would flood the bus.
189
+ - **`buildQualityMaskAtPromotion`, `buildQualityDriftScore`,
190
+ `meetsHgQualityRequirement` do NOT emit** — quality computation is
191
+ scoring, not a governance decision. They're called during promotion
192
+ decisions; the calling code emits the governance event.
193
+ - **Default bus is in-process only.** Multi-process buses (Redis-backed,
194
+ etc.) are a consumer choice — implement the `EventBus` interface and
195
+ call `setEventBus(yourBus)`.
196
+
197
+ ### Use with `@rocketlang/aegis-suite`
198
+
199
+ If you installed the meta-package, you can wire all 6 primitives in one call:
200
+
201
+ ```typescript
202
+ import { wireAllToBus } from '@rocketlang/aegis-suite'; // available in suite v0.2.0+
203
+ wireAllToBus(); // default: in-memory bus + SQLite writer to ~/.aegis/acc-events.db
204
+ ```
205
+
206
+ This sets up the bus on aegis-guard + chitta-detect + lakshmanrekha + hanumang-mandate
207
+ all at once, and persists events for the Agentic Control Center page.
208
+
209
+ ### Discipline
210
+
211
+ - **Stateless contract preserved.** Primitives hold no state beyond a
212
+ module-private bus reference. Pass `null` to `setEventBus` to detach.
213
+ - **Emission must never throw.** If your bus implementation throws,
214
+ the primitive's caller is unaffected — the receipt is silently dropped.
215
+ This is intentional; observability must not break the governed path.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@rocketlang/aegis-guard",
3
- "version": "0.1.0",
4
- "description": "AEGIS Guard SDK — reusable approval-token, nonce, idempotency, SENSE, and quality-evidence primitives for AEGIS-governed services",
3
+ "version": "0.2.0",
4
+ "description": "AEGIS Guard SDK — Five Locks primitives (approval-token, nonce, idempotency, SENSE, quality-evidence) + opt-in Agentic Control Center event bus for AEGIS-governed services",
5
5
  "license": "AGPL-3.0-only",
6
6
  "type": "module",
7
7
  "author": "Capt. Anil Sharma <capt.anil.sharma@powerpbox.org>",
package/src/acc-bus.ts ADDED
@@ -0,0 +1,71 @@
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/aegis-guard — opt-in ACC event bus integration (v0.2.0)
6
+ // @rule:ACC-003 — Opt-in event bus. 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. No bus = no emit.
9
+ // @rule:INF-ACC-005 — emit() is a no-op when no bus has been set.
10
+
11
+ /**
12
+ * Lightweight receipt shape — structurally compatible with the canonical
13
+ * AccReceipt in /root/aegis/src/acc/types.ts. Defined locally so
14
+ * this primitive can ship without depending on the ACC package.
15
+ */
16
+ export interface AccReceipt {
17
+ receipt_id: string;
18
+ primitive: string;
19
+ event_type: string;
20
+ emitted_at: string;
21
+ agent_id?: string;
22
+ verdict?: string;
23
+ rules_fired?: string[];
24
+ summary?: string;
25
+ payload?: Record<string, unknown>;
26
+ }
27
+
28
+ export interface EventBus {
29
+ emit(receipt: AccReceipt): void;
30
+ }
31
+
32
+ // Module-private bus reference. null by default — emission is no-op.
33
+ let _bus: EventBus | null = null;
34
+
35
+ /**
36
+ * Opt-in: provide an event bus to receive lightweight ACC receipts
37
+ * for every Five Locks primitive call. Pass null to detach.
38
+ *
39
+ * Default behaviour (no bus set): primitives behave EXACTLY as v0.1.0 —
40
+ * no emission, no state, no side effect beyond their primary return value.
41
+ *
42
+ * @rule:ACC-003 @rule:ACC-YK-003
43
+ */
44
+ export function setEventBus(bus: EventBus | null): void {
45
+ _bus = bus;
46
+ }
47
+
48
+ /**
49
+ * Internal helper — primitives call this to emit a receipt. No-op when
50
+ * no bus is set. MUST NOT throw — bus implementation handles delivery
51
+ * failures (ACC-YK-003 stateless contract).
52
+ *
53
+ * @rule:INF-ACC-005
54
+ */
55
+ export function emitAccReceipt(receipt: Omit<AccReceipt, 'primitive' | 'emitted_at'>): void {
56
+ if (!_bus) return;
57
+ try {
58
+ _bus.emit({
59
+ ...receipt,
60
+ primitive: 'aegis-guard',
61
+ emitted_at: new Date().toISOString(),
62
+ });
63
+ } catch {
64
+ // bus implementation failure must never break the primitive's caller
65
+ }
66
+ }
67
+
68
+ /** Test/introspection helper — does the primitive have a bus set right now? */
69
+ export function isBusWired(): boolean {
70
+ return _bus !== null;
71
+ }
@@ -5,6 +5,7 @@
5
5
  import { createHash } from 'crypto';
6
6
  import { IrrNoApprovalError } from './errors.js';
7
7
  import { type NonceStore, defaultNonceStore } from './nonce.js';
8
+ import { emitAccReceipt } from './acc-bus.js';
8
9
 
9
10
  // Token may arrive up to 60s before local clock (NTP tolerance).
10
11
  const CLOCK_SKEW_MS = 60_000;
@@ -41,48 +42,63 @@ export function verifyApprovalToken(
41
42
  expectedCapability: string,
42
43
  expectedOperation: string,
43
44
  ): ApprovalTokenPayload {
44
- let payload: ApprovalTokenPayload;
45
-
45
+ // @rule:ACC-003 @rule:ACC-004 — emit ACC receipt on success OR failure
46
+ const scope = `${expectedServiceId}/${expectedCapability}/${expectedOperation}`;
46
47
  try {
47
- const decoded = Buffer.from(token, 'base64url').toString('utf8');
48
- payload = JSON.parse(decoded) as ApprovalTokenPayload;
49
- } catch {
50
- throw new IrrNoApprovalError(expectedCapability, 'token could not be decoded');
51
- }
52
-
53
- if (payload.service_id !== expectedServiceId) {
54
- throw new IrrNoApprovalError(
55
- expectedCapability,
56
- `AEG-E-016: token scoped to '${payload.service_id}', not '${expectedServiceId}'`,
57
- );
58
- }
59
-
60
- if (payload.capability !== expectedCapability) {
61
- throw new IrrNoApprovalError(
62
- expectedCapability,
63
- `AEG-E-016: token capability '${payload.capability}' does not match '${expectedCapability}'`,
64
- );
65
- }
66
-
67
- if (payload.operation !== expectedOperation) {
68
- throw new IrrNoApprovalError(
69
- expectedCapability,
70
- `AEG-E-016: token operation '${payload.operation}' does not match '${expectedOperation}'`,
71
- );
72
- }
48
+ let payload: ApprovalTokenPayload;
49
+ try {
50
+ const decoded = Buffer.from(token, 'base64url').toString('utf8');
51
+ payload = JSON.parse(decoded) as ApprovalTokenPayload;
52
+ } catch {
53
+ throw new IrrNoApprovalError(expectedCapability, 'token could not be decoded');
54
+ }
73
55
 
74
- if (Date.now() > payload.expires_at) {
75
- throw new IrrNoApprovalError(expectedCapability, 'AEG-E-016: token expired');
76
- }
56
+ if (payload.service_id !== expectedServiceId) {
57
+ throw new IrrNoApprovalError(
58
+ expectedCapability,
59
+ `AEG-E-016: token scoped to '${payload.service_id}', not '${expectedServiceId}'`,
60
+ );
61
+ }
62
+ if (payload.capability !== expectedCapability) {
63
+ throw new IrrNoApprovalError(
64
+ expectedCapability,
65
+ `AEG-E-016: token capability '${payload.capability}' does not match '${expectedCapability}'`,
66
+ );
67
+ }
68
+ if (payload.operation !== expectedOperation) {
69
+ throw new IrrNoApprovalError(
70
+ expectedCapability,
71
+ `AEG-E-016: token operation '${payload.operation}' does not match '${expectedOperation}'`,
72
+ );
73
+ }
74
+ if (Date.now() > payload.expires_at) {
75
+ throw new IrrNoApprovalError(expectedCapability, 'AEG-E-016: token expired');
76
+ }
77
+ if (payload.issued_at !== undefined && payload.issued_at > Date.now() + CLOCK_SKEW_MS) {
78
+ throw new IrrNoApprovalError(
79
+ expectedCapability,
80
+ 'AEG-E-016: token issued_at is in the future (clock skew > 60s or forged timestamp)',
81
+ );
82
+ }
77
83
 
78
- if (payload.issued_at !== undefined && payload.issued_at > Date.now() + CLOCK_SKEW_MS) {
79
- throw new IrrNoApprovalError(
80
- expectedCapability,
81
- 'AEG-E-016: token issued_at is in the future (clock skew > 60s or forged timestamp)',
82
- );
84
+ emitAccReceipt({
85
+ receipt_id: `aegis-guard-verify-${digestApprovalToken(token)}`,
86
+ event_type: 'lock.approval.verified',
87
+ verdict: 'PASS',
88
+ rules_fired: ['AEG-E-016'],
89
+ summary: scope,
90
+ });
91
+ return payload;
92
+ } catch (err) {
93
+ emitAccReceipt({
94
+ receipt_id: `aegis-guard-verify-fail-${Date.now()}`,
95
+ event_type: 'lock.approval.rejected',
96
+ verdict: 'FAIL',
97
+ rules_fired: ['AEG-E-016'],
98
+ summary: `${scope} — ${(err as Error).message?.slice(0, 160) ?? 'verification failed'}`,
99
+ });
100
+ throw err;
83
101
  }
84
-
85
- return payload;
86
102
  }
87
103
 
88
104
  // @rule:AEG-HG-2B-006 — consume nonce before any state mutation; missing nonce = hard reject.
@@ -91,19 +107,39 @@ export async function verifyAndConsumeNonce(
91
107
  payload: ApprovalTokenPayload,
92
108
  store: NonceStore = defaultNonceStore,
93
109
  ): Promise<void> {
94
- if (!payload.nonce) {
95
- throw new IrrNoApprovalError(
96
- payload.capability,
97
- 'AEG-E-016: irreversible operation requires nonce for replay prevention',
98
- );
99
- }
100
- const ttlMs = Math.max(0, payload.expires_at - Date.now());
101
- const consumed = await store.consumeNonce(payload.nonce, ttlMs);
102
- if (!consumed) {
103
- throw new IrrNoApprovalError(
104
- payload.capability,
105
- `AEG-E-016: nonce '${payload.nonce}' already consumed — approval replay rejected`,
106
- );
110
+ // @rule:ACC-003 @rule:ACC-004 — emit ACC receipt on success OR failure
111
+ const scope = `${payload.service_id}/${payload.capability}/${payload.operation}`;
112
+ try {
113
+ if (!payload.nonce) {
114
+ throw new IrrNoApprovalError(
115
+ payload.capability,
116
+ 'AEG-E-016: irreversible operation requires nonce for replay prevention',
117
+ );
118
+ }
119
+ const ttlMs = Math.max(0, payload.expires_at - Date.now());
120
+ const consumed = await store.consumeNonce(payload.nonce, ttlMs);
121
+ if (!consumed) {
122
+ throw new IrrNoApprovalError(
123
+ payload.capability,
124
+ `AEG-E-016: nonce '${payload.nonce}' already consumed — approval replay rejected`,
125
+ );
126
+ }
127
+ emitAccReceipt({
128
+ receipt_id: `aegis-guard-nonce-${payload.nonce}`,
129
+ event_type: 'lock.nonce.consumed',
130
+ verdict: 'PASS',
131
+ rules_fired: ['AEG-HG-2B-006'],
132
+ summary: scope,
133
+ });
134
+ } catch (err) {
135
+ emitAccReceipt({
136
+ receipt_id: `aegis-guard-nonce-fail-${Date.now()}`,
137
+ event_type: 'lock.nonce.rejected',
138
+ verdict: 'FAIL',
139
+ rules_fired: ['AEG-HG-2B-006'],
140
+ summary: `${scope} — ${(err as Error).message?.slice(0, 160) ?? 'nonce check failed'}`,
141
+ });
142
+ throw err;
107
143
  }
108
144
  }
109
145
 
@@ -1,6 +1,8 @@
1
1
  // @rule:AEG-HG-2B-006 — idempotency protects the operation; nonce protects the approval (separate locks)
2
2
  // Pattern: check DB for externalRef before mutating. Matching fingerprint = safe no-op. Mismatch = warn + reject.
3
3
 
4
+ import { emitAccReceipt } from './acc-bus.js';
5
+
4
6
  export interface IdempotencyCheckResult {
5
7
  isDuplicate: boolean;
6
8
  payloadMismatch: boolean;
@@ -22,11 +24,21 @@ export function checkIdempotency(
22
24
  }
23
25
  const payloadMismatch =
24
26
  existingFingerprint !== undefined && existingFingerprint !== newFingerprint;
25
- return {
27
+ const result: IdempotencyCheckResult = {
26
28
  isDuplicate: true,
27
29
  payloadMismatch,
28
30
  safeNoOp: !payloadMismatch,
29
31
  };
32
+ emitAccReceipt({
33
+ receipt_id: `aegis-guard-idem-${_externalRef}`,
34
+ event_type: payloadMismatch ? 'lock.idempotency.mismatch' : 'lock.idempotency.duplicate',
35
+ verdict: payloadMismatch ? 'WARN' : 'PASS',
36
+ rules_fired: ['AEG-HG-2B-006'],
37
+ summary: payloadMismatch
38
+ ? `duplicate externalRef ${_externalRef} with payload mismatch — caller must reject or escalate`
39
+ : `duplicate externalRef ${_externalRef} — safe no-op, return existing`,
40
+ });
41
+ return result;
30
42
  }
31
43
 
32
44
  // Build a stable base64 fingerprint from an arbitrary operation payload.
package/src/index.ts CHANGED
@@ -1,8 +1,19 @@
1
1
  // @rocketlang/aegis-guard — AEGIS Guard SDK public API
2
2
  // Five Locks proved in carbonx-backend (batches 62-74). Batch 93 makes them reusable.
3
+ // v0.2.0 adds opt-in Agentic Control Center (ACC) event bus integration.
3
4
 
4
5
  export { IrrNoApprovalError, AegisNonceError } from './errors.js';
5
6
 
7
+ // @rule:ACC-003 — Opt-in event bus for Agentic Control Center observability.
8
+ // Stateless contract preserved (ACC-YK-003): emit is no-op
9
+ // when setEventBus has not been called. v0.2.0+.
10
+ export {
11
+ type AccReceipt,
12
+ type EventBus,
13
+ setEventBus,
14
+ isBusWired,
15
+ } from './acc-bus.js';
16
+
6
17
  export { type NonceStore, defaultNonceStore } from './nonce.js';
7
18
 
8
19
  export {
package/src/sense.ts CHANGED
@@ -33,6 +33,26 @@ export function configureSenseTransport(transport: SenseTransport): void {
33
33
 
34
34
  // @rule:CA-003 — all three snapshot fields are required by the type; callers must supply them.
35
35
  // approval_token_ref, if present, must already be the output of digestApprovalToken (AEG-HG-2B-005).
36
+ // @rule:ACC-003 — also emit an ACC receipt for cockpit observability (no-op when bus unset).
36
37
  export function emitAegisSenseEvent(event: AegisSenseEvent): void {
37
38
  _transport(event);
39
+ emitAccReceiptFromSense(event);
40
+ }
41
+
42
+ import { emitAccReceipt } from './acc-bus.js';
43
+
44
+ function emitAccReceiptFromSense(event: AegisSenseEvent): void {
45
+ emitAccReceipt({
46
+ receipt_id: `aegis-guard-sense-${event.correlation_id || Date.now()}`,
47
+ event_type: 'lock.sense.emitted',
48
+ verdict: event.irreversible ? 'WARN' : 'PASS',
49
+ rules_fired: ['CA-003', 'AEG-HG-2B-003', 'AEG-HG-2B-005'],
50
+ summary: `${event.service_id}/${event.capability}/${event.operation} ${event.irreversible ? '(irreversible)' : ''}`,
51
+ payload: {
52
+ event_type: event.event_type,
53
+ correlation_id: event.correlation_id,
54
+ approval_token_ref: event.approval_token_ref,
55
+ delta: event.delta,
56
+ },
57
+ });
38
58
  }