@blamejs/core 0.9.24 → 0.9.28
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/CHANGELOG.md +4 -0
- package/index.js +15 -1
- package/lib/agent-audit.js +45 -0
- package/lib/agent-event-bus.js +336 -0
- package/lib/agent-idempotency.js +2 -8
- package/lib/agent-orchestrator.js +2 -8
- package/lib/agent-posture-chain.js +208 -0
- package/lib/agent-saga.js +191 -0
- package/lib/agent-stream.js +2 -8
- package/lib/agent-tenant.js +308 -0
- package/lib/guard-event-bus-payload.js +217 -0
- package/lib/guard-event-bus-topic.js +150 -0
- package/lib/guard-posture-chain.js +201 -0
- package/lib/guard-saga-config.js +157 -0
- package/lib/guard-tenant-id.js +138 -0
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,10 @@ upgrading across more than a few patches at a time.
|
|
|
8
8
|
|
|
9
9
|
## v0.9.x
|
|
10
10
|
|
|
11
|
+
- v0.9.28 (2026-05-14) — **`b.agent.postureChain` — set-based compliance posture propagated across every agent boundary.** Eighth substrate slice. Compliance regimes (HIPAA / PCI-DSS / GDPR / SOC2) protect DIFFERENT regulated-data classes — they're orthogonal, not a linear lattice. Set semantics match how real-world regulations actually overlap (a clinic that processes payment cards operates under BOTH HIPAA + PCI). (1) **`b.agent.postureChain.create({ audit })`** — facade with `isSubset(targetSet, sourceSet)` (the core check: target.set ⊇ source.set), `union(...sets)`, `canDelegate(sourceSet, targetSet, methodName)`, `appendHop(envelope, hopName)`, `validate(envelope, agentPostureSet)`, `declareRegime(name)`, `REGIMES` (frozen array). (2) **Cross-boundary downgrade refusal** — `validate(envelope, agentPostureSet)` throws `agent-posture-chain/downgrade-refused` when the target (agent) posture set is missing any regime the source (envelope) carries. Audit emit at HEIGHTENED with the missing regime list + chain trail so operator review pipelines surface every downgrade attempt. (3) **Hop trail tracking** — `appendHop(envelope, hopName)` immutably extends `{ chainTrail, enteredAt, hopCount }`. Hop count caps at default 16 — defends infinite recursion across agent delegation. Per-hop timestamps must be monotonic non-negative numbers (guard rejects non-monotonic, length mismatches, etc.). (4) **`b.guardPostureChain`** — envelope shape validator. Refuses oversized hop trail (cap = 16 hops strict), non-ASCII hop names (operator-greppable), C0 / DEL forbidden, duplicate hops in trail (recursion guard), duplicate regimes in `postureSet`, non-monotonic `enteredAt` timestamps, `enteredAt` length mismatch with `chainTrail`. (5) **Built-in regimes**: HIPAA, PCI-DSS, GDPR, SOC2. Operator declares custom regimes via `chain.declareRegime("healthcare-tier-1")`; duplicate declarations refused. (6) **Empty source ⊆ any target** — unscoped calls (no posture context) flow freely; only declared source regimes constrain the target. Fuzz harness ships in `fuzz/guard-posture-chain.fuzz.js`. Per the substrate playbook in `memory/specs/blamejs-agent-posture-chain-spec.md`.
|
|
12
|
+
- v0.9.27 (2026-05-14) — **`b.agent.saga` — multi-step coordination with compensation cascade.** Seventh substrate slice. (1) **`b.agent.saga.create({ name, steps, audit })`** — every step has `{ name, run: async (ctx, state) => {}, compensate?: async (ctx, state) => {} }`. `saga.run(ctx, initialState, opts)` walks steps in order; each `run` mutates the shared `state` object. On step throw the framework fires every previously-completed step's `compensate` in REVERSE order (steps that hadn't completed aren't compensated). (2) **Compensation failure semantics** — a compensate that throws emits `agent.saga.compensation_failed` audit at CRITICAL severity, halts further compensations, and the saga's final error message carries both the original step failure + the compensation failure so operator alert pipelines see the full context. (3) **No saga-level retry** — operator-confirmed 2026-05-14: saga's value-add is COMPENSATION, not retry. Step.run wraps `b.retry` if needed (with v0.9.22 idempotency available, internal retry inside step.run is side-effect-safe). Keeps the saga primitive focused on the coordination contract. (4) **Audit lifecycle** — `agent.saga.started` / `step_started` / `step_completed` / `step_failed` / `compensation_started` / `compensation_completed` / `compensation_failed` / `failed` / `completed`. Each event carries `sagaId` + `name` + `stepName` + `stepIndex` for operator dashboards. (5) **`b.guardSagaConfig`** — saga-creation config validator. Refuses empty steps array, duplicate step names, non-function `run`, non-function `compensate` (when provided), non-ASCII saga name, oversized step count (default 32). Ships profile + posture vocabulary uniform with rest of guard family. Fuzz harness ships in `fuzz/guard-saga-config.fuzz.js`. Per the substrate playbook in `memory/specs/blamejs-agent-saga-spec.md`.
|
|
13
|
+
- v0.9.26 (2026-05-14) — **`b.agent.tenant` — multi-tenant isolation as a first-class primitive.** Sixth substrate slice. Replaces the per-operator wiring of `actor.tenantId === registeredTenant` (which tends to leak across handlers) with one centralized scope. (1) **`b.agent.tenant.create({ backend, audit, permissions })`** — facade with `register(tenantId, { posture, archivePolicy, metadata })`, `unregister(tenantId, opts)`, `lookup(tenantId)`, `list({})`, `check(actor, agentTenantId)`, `derivedKey(tenantId, purpose)`, `auditFor(tenantId)`, `listArchived()`. Pluggable backend; in-memory default. (2) **Cross-tenant gate (`check`)** — refuses calls where `actor.tenantId !== agentTenantId` unless the actor holds `framework-cross-tenant-admin` scope (every cross-tenant access by an admin emits `agent.tenant.cross_tenant_access` audit at HEIGHTENED severity for operator review). (3) **Per-tenant derived keys (`derivedKey`)** — composes `b.crypto.namespaceHash` for deterministic per-tenant key derivation from `purpose` + `tenantId`. Cross-tenant decrypt refused at the vault boundary by construction — each tenant's seal-key derivation differs, so even with disk access an attacker can't cross-decrypt. (4) **Per-tenant audit (`auditFor`)** — returns an audit wrapper that auto-tags every emit with `metadata.tenantId` so each tenant's audit trail is independently filterable in the operator's audit pipeline. (5) **Archive-default destroy semantics** — `unregister(tenantId)` archives by default (retention-safe, matches SEC 17a-4 6yr / FINRA 4511 / HIPAA §164.530(j) / MiFID II Art 16(7) compliance needs). Destruction requires explicit `{ destroy: true, stepUpToken, dualControlApprover, reason, actor }` — four preconditions all required together. The framework validates the SHAPE; the operator's step-up middleware + dual-control middleware grants the actual tokens upstream. Missing any precondition refuses with a specific `agent-tenant/destroy-requires-step-up` / `-dual-control` / `-reason` / `-actor` code so operators see exactly what's missing. (6) **`b.guardTenantId`** — tenant-id shape validator. Refuses non-ASCII (operator-greppable in audit logs across stack boundaries), path-traversal (`..` / `/` / `\` / NUL / C0 / DEL), oversized (default 64 bytes), reserved `ROOT` / `FRAMEWORK` / `*`, leading `.`. Posture vocabulary uniform (`strict` / `balanced` / `permissive` profiles; hipaa / pci-dss / gdpr / soc2 postures pin strict). Fuzz harness ships in `fuzz/guard-tenant-id.fuzz.js`; ClusterFuzzLite matrix entry added. Per the substrate playbook in `memory/specs/blamejs-agent-tenant-spec.md`.
|
|
14
|
+
- v0.9.25 (2026-05-14) — **`b.agent.eventBus` — typed cross-agent publish/subscribe with schema enforcement, posture-tagged topics, and cross-tenant subscribe refusal.** Fifth substrate slice. Substrate for every agent-to-agent reaction the framework will need (`mail.scan.malware-detected` → MX refuses source, `mail.crypto.key-rotated` → vault invalidates cached recipient keys, `ai.classify.prompt-injection-detected` → agent quarantines). (1) **`b.agent.eventBus.create({ pubsub, audit, permissions })`** — facade over operator-supplied `b.pubsub` (or any `{ publish, subscribe, unsubscribe }`-shaped backend). Surface: `registerTopic(name, { schema, posture, permissions, tenantScope })`, `publish(name, payload, { actor })`, `subscribe(name, handler, { actor })` returns unsubscribe fn, `listTopics({ actor })`. (2) **Topic registration with schema** — declared at boot via flat key→type map (`string` / `number` / `boolean` / `integer` / `isoDateTime` / `array` / `object`; suffix `?` marks optional). Unknown topics refuse publish + subscribe so typos fail loudly. Duplicate registration refused (operator must `unregister` first when the surface gains that surface in a later substrate slice). (3) **Schema enforcement at every boundary** — every payload validated against schema before `pubsub.publish` AND at each delivery (subscriber-side re-validation defends in-flight tampering). Refused payloads emit `agent.event_bus.delivery_dropped` audit; the publisher's call throws. (4) **Permission gating** — `permissions.publish` + `permissions.subscribe` per-topic scope arrays; `b.permissions.check(actor, scope)` on every publish + subscribe call. Mismatch emits `agent.event_bus.publish_denied` / `subscribe_denied` audit at HEIGHTENED severity. (5) **Cross-tenant isolation** — `tenantScope: true` topics carry the publisher's `tenantId` in the wire envelope; subscriber's actor must declare a matching tenantId or the event is silently dropped at delivery with `agent.event_bus.cross_tenant_drop` audit. (6) **`b.guardEventBusTopic`** — topic name validator. Refuses dot-count < 3 (operators use `<domain>.<source>.<event>` shape), oversized (default 128 bytes), non-ASCII (operator-greppable in audit logs across cross-process boundaries), reserved `framework.*` prefix, path-traversal shapes, slash, backslash, C0 / DEL. (7) **`b.guardEventBusPayload`** — payload schema validator. Bounded byte cap (default 64 KiB — events are metadata, not bulk data; publishers reference bulk data via `b.objectStore` IDs). Type-check cascade refuses non-finite numbers, malformed ISO-8601 dateTime (length-bound to 64 chars before regex test so a hostile input can't burn regex-engine CPU), missing required fields, unknown fields (schemas must be exhaustive). (8) **Audit lifecycle** — `agent.event_bus.topic_registered` / `published` / `subscribed` / `publish_denied` / `subscribe_denied` / `delivery_dropped` / `cross_tenant_drop` / `handler_threw`. (9) **Shared agent-audit helper extracted** — `lib/agent-audit.js` factored out the identical `_safeAudit` wrapper that v0.9.21 orchestrator + v0.9.22 idempotency + v0.9.24 stream + v0.9.25 eventBus all carried inline. The four agent substrate modules now compose `agentAudit.safeAudit`. Fuzz harnesses ship in `fuzz/guard-event-bus-topic.fuzz.js` + `fuzz/guard-event-bus-payload.fuzz.js`. Per the substrate playbook in `memory/specs/blamejs-agent-event-bus-spec.md`.
|
|
11
15
|
- v0.9.24 (2026-05-14) — **`b.agent.stream` — async-iterable variants for agent methods that yield N rows.** Fourth slice of the v0.9.21–v0.9.30 substrate playbook (numbers shifted +1 by v0.9.23 CodeQL insertion). JMAP `Email/queryChanges` against million-message mailboxes returns an array today — OOM on client + agent before the response ships. RFC 8620 §5.5 allows `position`-paginated responses; real mailboxes need cursor-backed delivery with built-in backpressure. (1) **`b.agent.stream.create({ openCursor, cursorOpts, batchSize, orchestrator, actor, kind, audit })`** — returns an object implementing `[Symbol.asyncIterator]` so operators write `for await (var row of stream) { ... }`. Operator supplies an `openCursor(cursorOpts)` factory returning a cursor with `fetchBatch(batchSize) → { rows, nextCursor, done }` + optional `close()` + optional `lastSeenCursor()`. (2) **Backpressure built-in** — async-generator semantics; each `next()` call yields one row from an in-memory batch buffer; the cursor only refetches when the buffer drains. Pulling slowly applies backpressure to the store side; a slow client can't OOM the server. (3) **Auto cursor close on every exit path** — consumer `break` calls iterator `.return()`, consumer `throw` calls `.throw()`, natural exhaustion calls `.next()` until done — all three close the cursor via `try`/`finally`-style `_closeOnce` and emit `agent.stream.closed` audit with reason (`exhausted` / `consumer-break` / `consumer-throw` / `drain` / `error`). (4) **Drain marker** — when orchestrator drain fires mid-stream, the next `next()` call returns ONE final `{ _drainMarker: true, lastSeenCursor: <opaque>, reason: "drain" }` row + closes the cursor. Clients reconnecting via JMAP-WebSocket / IMAP NOTIFY pass `lastSeenCursor` back to resume from the same position against the new agent post-deploy. Composes v0.9.21 `orchestrator.registerStream` / `unregisterStream` / `isDraining` hooks. (5) **`b.guardStreamArgs`** — validates `b.agent.stream.create` opts. Refuses non-integer `batchSize` (silent shard-style routing drift class — same shape Codex caught on v0.9.21), batchSize out of `[1, 1024]` strict-profile range, empty `kind` string, function / regex / Buffer / `__proto__` keys inside `cursorOpts` (structured-clone-unsafe — same shape `b.guardMailQuery` refuses for filter specs). Ships `strict` / `balanced` / `permissive` profiles + hipaa / pci-dss / gdpr / soc2 postures. (6) Audit lifecycle: `agent.stream.opened` on iterator creation, `agent.stream.closed` on every exit path, `agent.stream.drain_marker_emitted` when orch drain fires. Fuzz harness ships in `fuzz/guard-stream-args.fuzz.js`. Per the substrate playbook in `memory/specs/blamejs-agent-stream-spec.md`.
|
|
12
16
|
- v0.9.23 (2026-05-14) — **CodeQL alert sweep — 30 alerts closed across 6 rule classes + wiki @primitive validator extracted into static gates.** Standalone substrate slice inserted into the v0.9.21–v0.9.30 playbook; downstream substrate slices shift +1 (stream now v0.9.24, eventBus v0.9.25, tenant v0.9.26, saga v0.9.27, postureChain v0.9.28, trace v0.9.29, snapshot v0.9.30). The 30 alerts were regressions of v0.9.18's earlier batch fix — v0.9.15's framework-wide `require()`-binding rename sweep (184 renames across 108 files) shifted line numbers and the suppression comments stopped tracking. (1) **`js/file-system-race` (10 sites)** — `lib/atomic-file.js:_readSyncCore` migrated from `statSync`+`readFileSync` to the canonical TOCTOU-safe-read scaffold (open fd → `fstatSync` → `readSync` loop → `closeSync` in finally; ENOENT now surfaces from `openSync`); `lib/restore-rollback.js` marker write switched to `openSync(..., "wx", 0o600)` + `writeSync` + `fsyncSync` + EEXIST tolerance; `lib/network-tls.js` new `_readPathFile` fd-based reader; `lib/backup/bundle.js` fd-narrowed read with short-read detection; `lib/static.js` content-safety gate converted to `fsp.open` filehandle pattern; `lib/vault/seal-pem-file.js`, `lib/atomic-file.js:fsyncDir`, `test/30-chain.js`, `examples/wiki/test/validate-{env,cli}-snapshot.js`, `examples/wiki/lib/source-doc-parser.js` got suppression refresh OR fd-narrowed refactor per shape. (2) **`js/insecure-temporary-file` (6 sites)** — `lib/mtls-ca.js` new `_writeExclusive` helper using `openSync(..., "wx", mode)` + `writeSync` + `fsyncSync`; `lib/vault/rotate.js` + `lib/http-client.js` got suppression refresh pointing at the operator-supplied stagingDir / 64-bit CSPRNG suffix defenses. (3) **`js/path-injection` (2 sites in `lib/static.js`)** — suppression refresh pointing at the upstream `_resolveSafe` sandbox check at line 181 (lexical resolve + `startsWith(rootResolved + sep)` + realpath escape guard + guardFilename gate). (4) **`js/remote-property-injection` (7 sites)** — `lib/middleware/csrf-protect.js` cookie-parse output + `lib/websocket.js` `_parseExtensionHeader` params switched to `Object.create(null)` so attacker-controlled keys have no prototype chain to pollute; `lib/middleware/body-parser.js` multipart fields got suppression refresh pointing at the POISONED_KEYS gate at line 867; `test/40-consumers.js` + `test/00-primitives.js` test fixtures suppressed with `test/` scope justification. (5) **`PinnedDependenciesID` (2 workflow files)** — every `uses:` line was already SHA-pinned per v0.9.18; the remaining alerts pointed at the `npm install --no-audit --no-fund` step in `.github/workflows/npm-publish.yml` + `ci.yml`; added explicit `name@version` specifiers (esbuild + postject, mirroring `package.json`'s exact pins) so CodeQL recognizes the install as pinned without committing a lockfile (which CLAUDE.md hard rule §1 forbids — vendored stack, zero npm runtime deps). (6) **`js/regex/missing-regexp-anchor` (1 site)** — `scripts/build-vendored-sbom.js:42` host-extractor regex anchored to `^https?://github.com/` so an attacker-controlled `entry.source` containing `github.com` as a path or query substring can't misdirect purl dispatch into the github branch. (7) **Wiki @primitive validator extracted into static gates** — the `@related namespace-not-primitive` nit fired on PRs #50 (v0.9.20), #51 (v0.9.21), AND #52 (v0.9.22) — same class, three rediscoveries because the validator only ran in CI's wiki-e2e gate (~90s round-trip), never in local pre-push. Extraction: `examples/wiki/lib/source-comment-block-validator.js` (shared engine, pure module, no side effects) + `scripts/validate-source-comment-blocks.js` (framework-level standalone wrapper, runs in 419–466ms, no `@blamejs/core` / vault / DB / network dependencies). Existing wiki-e2e gate refactored to delegate to the shared engine; CI invocation unchanged. CLAUDE.md release workflow step 4 (static gates) now lists `node scripts/validate-source-comment-blocks.js` after `codebase-patterns.test.js`. Each suppression comment references the active defense by file:line so future CodeQL re-runs OR future readers know which suppression applies. Multi-agent fan-out — 4 parallel subagents in isolated git worktrees per rule class.
|
|
13
17
|
- v0.9.22 (2026-05-14) — **`b.agent.idempotency` — cross-dispatch idempotency keys honored at every agent consumer boundary; JMAP retry-safe semantics from day one.** Second slice of the v0.9.21–v0.9.29 substrate playbook. (1) **`b.agent.idempotency.create({ store, audit, ttlMs, maxResultBytes, fingerprintArgs })`** — pluggable backing store via `{ get, put, delete, gc }`; in-memory default ships for single-process deployments, operator wires durable backends (sqlite-backed adapter or external). Surface: `instance.get(method, actorId, key)` returns cached envelope `{ result, firstAt, lastReplayedAt, replayCount, requestFingerprint }` or `null`; `instance.put(method, actorId, key, result, { args, requestFingerprint })` serializes via `b.safeJson.stringify` + persists with TTL + refuses key-reuse-different-args via the request-fingerprint (sha3-512 of args sans key) check; `instance.invalidate(method, actorId, key)` is the saga-compensation escape hatch; `instance.gc({ olderThanMs })` for periodic cleanup. (2) **Keys hashed at the boundary** — operator-supplied keys never reach disk in raw form; namespace-hashed via `b.crypto.namespaceHash("agent.idempotency", method + "\\0" + actorId + "\\0" + key)`. Cross-actor + cross-method isolation by construction (an attacker can't replay another actor's mutation by guessing the key). (3) **Replay tracking** — every cache hit increments `replayCount` + bumps `lastReplayedAt`; audit emits `agent.idempotency.replay` with the truncated actor-id hash + first/replay timestamps so operator pipelines surface retry storms. (4) **`b.guardIdempotencyKey`** — operator-supplied key shape validator. Refuses oversized (default 256 bytes), control chars (C0/NUL/DEL — defends audit-log injection), slash + backslash (defends operators routing keys through filesystem paths), path-traversal (`..`), non-ASCII under strict (operator-greppable in audit logs across stack boundaries; permissive opts down for legacy Unicode tenant IDs). (5) **JSON round-trip test helper** — new `test/helpers/json-round-trip.js` exporting `assertJsonRoundTrip(shape, label)`. Catches the bug class Codex flagged on PR #51 (v0.9.21 `register()` stored agent function ref on backend row, lost in DB/JSON serialization). Refuses on function fields, Buffer fields, Date objects, Symbol keys, BigInt values, non-finite numbers, cycles. Applied retroactively to v0.9.21 agent-orchestrator backend row in regression test. (6) **Result size cap** (default 1 MiB per cached entry) — refuses with `agent-idempotency/result-too-big` so operators discover OOM-prone result shapes locally instead of at runtime. Fuzz harness ships in `fuzz/guard-idempotency-key.fuzz.js`; ClusterFuzzLite matrix entries added. Per the substrate playbook in `memory/specs/blamejs-agent-idempotency-spec.md`.
|
package/index.js
CHANGED
|
@@ -168,9 +168,18 @@ var guardMailSieve = require("./lib/guard-mail-sieve");
|
|
|
168
168
|
var guardAgentRegistry = require("./lib/guard-agent-registry");
|
|
169
169
|
var guardIdempotencyKey = require("./lib/guard-idempotency-key");
|
|
170
170
|
var guardStreamArgs = require("./lib/guard-stream-args");
|
|
171
|
+
var guardEventBusTopic = require("./lib/guard-event-bus-topic");
|
|
172
|
+
var guardEventBusPayload = require("./lib/guard-event-bus-payload");
|
|
173
|
+
var guardTenantId = require("./lib/guard-tenant-id");
|
|
174
|
+
var guardSagaConfig = require("./lib/guard-saga-config");
|
|
175
|
+
var guardPostureChain = require("./lib/guard-posture-chain");
|
|
171
176
|
var agentOrchestrator = require("./lib/agent-orchestrator");
|
|
172
177
|
var agentIdempotency = require("./lib/agent-idempotency");
|
|
173
178
|
var agentStream = require("./lib/agent-stream");
|
|
179
|
+
var agentEventBus = require("./lib/agent-event-bus");
|
|
180
|
+
var agentTenant = require("./lib/agent-tenant");
|
|
181
|
+
var agentSaga = require("./lib/agent-saga");
|
|
182
|
+
var agentPostureChain = require("./lib/agent-posture-chain");
|
|
174
183
|
var guardArchive = require("./lib/guard-archive");
|
|
175
184
|
var guardJson = require("./lib/guard-json");
|
|
176
185
|
var guardYaml = require("./lib/guard-yaml");
|
|
@@ -417,7 +426,12 @@ module.exports = {
|
|
|
417
426
|
guardAgentRegistry: guardAgentRegistry,
|
|
418
427
|
guardIdempotencyKey: guardIdempotencyKey,
|
|
419
428
|
guardStreamArgs: guardStreamArgs,
|
|
420
|
-
|
|
429
|
+
guardEventBusTopic: guardEventBusTopic,
|
|
430
|
+
guardEventBusPayload: guardEventBusPayload,
|
|
431
|
+
guardTenantId: guardTenantId,
|
|
432
|
+
guardSagaConfig: guardSagaConfig,
|
|
433
|
+
guardPostureChain: guardPostureChain,
|
|
434
|
+
agent: { orchestrator: agentOrchestrator, idempotency: agentIdempotency, stream: agentStream, eventBus: agentEventBus, tenant: agentTenant, saga: agentSaga, postureChain: agentPostureChain },
|
|
421
435
|
guardArchive: guardArchive,
|
|
422
436
|
guardJson: guardJson,
|
|
423
437
|
guardYaml: guardYaml,
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* b.agent._audit — internal shared audit-emit helper for the agent
|
|
4
|
+
* substrate (`b.agent.orchestrator` / `b.agent.idempotency` /
|
|
5
|
+
* `b.agent.stream` / `b.agent.eventBus` / future substrate slices).
|
|
6
|
+
*
|
|
7
|
+
* Each agent primitive emits audit events at lifecycle boundaries
|
|
8
|
+
* (registered / opened / closed / replay / denied / drop / etc). The
|
|
9
|
+
* emit logic is identical: actor shape → audit.safeEmit() → swallow
|
|
10
|
+
* any audit-side failures. Extracted here so the 4+ agent substrate
|
|
11
|
+
* modules don't re-implement the same wrapper.
|
|
12
|
+
*
|
|
13
|
+
* Internal — operator-facing surface is each primitive's `.audit`
|
|
14
|
+
* opt; this is the implementation detail.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
function safeAudit(auditImpl, action, actor, metadata) {
|
|
18
|
+
try {
|
|
19
|
+
auditImpl.safeEmit({
|
|
20
|
+
action: action,
|
|
21
|
+
actor: actor ? { id: actor.id, roles: actor.roles || [] } : { id: "<system>" },
|
|
22
|
+
outcome: _outcomeFor(action),
|
|
23
|
+
metadata: metadata || {},
|
|
24
|
+
});
|
|
25
|
+
} catch (_e) { /* drop-silent — audit failures don't crash the call */ }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// "denied" / "drop" / "threw" / "different_args" / "miss" / "not_implemented"
|
|
29
|
+
// all imply failure outcome; anything else is success. Per-primitive
|
|
30
|
+
// classification can override by passing a metadata.outcome — that's
|
|
31
|
+
// merged in by the caller, not here.
|
|
32
|
+
function _outcomeFor(action) {
|
|
33
|
+
if (typeof action !== "string") return "success";
|
|
34
|
+
if (action.indexOf("denied") >= 0) return "failure";
|
|
35
|
+
if (action.indexOf("drop") >= 0) return "failure";
|
|
36
|
+
if (action.indexOf("threw") >= 0) return "failure";
|
|
37
|
+
if (action.indexOf("different_args") >= 0) return "failure";
|
|
38
|
+
if (action.indexOf("miss") >= 0) return "failure";
|
|
39
|
+
if (action.indexOf("not_implemented") >= 0) return "failure";
|
|
40
|
+
return "success";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
module.exports = {
|
|
44
|
+
safeAudit: safeAudit,
|
|
45
|
+
};
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @module b.agent.eventBus
|
|
4
|
+
* @nav Agent
|
|
5
|
+
* @title Agent Event Bus
|
|
6
|
+
* @order 65
|
|
7
|
+
*
|
|
8
|
+
* @intro
|
|
9
|
+
* Typed cross-agent publish/subscribe on top of `b.pubsub` (or any
|
|
10
|
+
* pubsub-shaped instance with `publish` / `subscribe` /
|
|
11
|
+
* `unsubscribe`). Substrate for every agent-to-agent reaction the
|
|
12
|
+
* mail stack + future agents need: `mail.scan.malware-detected` →
|
|
13
|
+
* MX refuses source, `mail.crypto.key-rotated` → vault invalidates
|
|
14
|
+
* cached recipient keys, `ai.classify.prompt-injection-detected` →
|
|
15
|
+
* agent quarantines, etc.
|
|
16
|
+
*
|
|
17
|
+
* The bus owns:
|
|
18
|
+
*
|
|
19
|
+
* - **Topic registry** — `registerTopic(name, { schema, posture,
|
|
20
|
+
* permissions, tenantScope })` declares the wire contract at
|
|
21
|
+
* boot. Unknown topics refuse publish + subscribe so typos
|
|
22
|
+
* fail loudly.
|
|
23
|
+
* - **Schema enforcement** — every payload validated against the
|
|
24
|
+
* declared schema before publish AND at each delivery
|
|
25
|
+
* (defends in-flight tampering).
|
|
26
|
+
* - **Permission gating** — `b.permissions.check(actor, scope)`
|
|
27
|
+
* on every publish + subscribe.
|
|
28
|
+
* - **Posture re-validation at delivery** — same shape as
|
|
29
|
+
* v0.9.20 cross-queue posture check.
|
|
30
|
+
* - **Audit lifecycle** — publish / subscribe / delivery / refused
|
|
31
|
+
* events emit to the operator's audit chain.
|
|
32
|
+
*
|
|
33
|
+
* ```js
|
|
34
|
+
* var bus = b.agent.eventBus.create({
|
|
35
|
+
* pubsub: myPubsub,
|
|
36
|
+
* audit: b.audit,
|
|
37
|
+
* permissions: myPerms,
|
|
38
|
+
* });
|
|
39
|
+
*
|
|
40
|
+
* bus.registerTopic("mail.scan.malware-detected", {
|
|
41
|
+
* schema: {
|
|
42
|
+
* source: "string",
|
|
43
|
+
* confidence: "number",
|
|
44
|
+
* detectedAt: "isoDateTime",
|
|
45
|
+
* },
|
|
46
|
+
* posture: "soc2",
|
|
47
|
+
* permissions: {
|
|
48
|
+
* publish: ["mail-scan:write"],
|
|
49
|
+
* subscribe: ["mail-mx:write"],
|
|
50
|
+
* },
|
|
51
|
+
* });
|
|
52
|
+
*
|
|
53
|
+
* await bus.publish("mail.scan.malware-detected", {
|
|
54
|
+
* source: "1.2.3.4", confidence: 0.95, detectedAt: new Date().toISOString(),
|
|
55
|
+
* }, { actor: { id: "scan-agent", roles: ["mail-scan-internal"] } });
|
|
56
|
+
* ```
|
|
57
|
+
*
|
|
58
|
+
* @card
|
|
59
|
+
* Typed cross-agent publish/subscribe. Topics registered with schema
|
|
60
|
+
* + posture + permissions; every payload validated; subscriber-side
|
|
61
|
+
* posture re-validated at delivery so no posture downgrade survives
|
|
62
|
+
* the bus boundary.
|
|
63
|
+
*/
|
|
64
|
+
|
|
65
|
+
var lazyRequire = require("./lazy-require");
|
|
66
|
+
var { defineClass } = require("./framework-error");
|
|
67
|
+
var guardEventBusTopic = require("./guard-event-bus-topic");
|
|
68
|
+
var guardEventBusPayload = require("./guard-event-bus-payload");
|
|
69
|
+
var agentAudit = require("./agent-audit");
|
|
70
|
+
|
|
71
|
+
var audit = lazyRequire(function () { return require("./audit"); });
|
|
72
|
+
|
|
73
|
+
var AgentEventBusError = defineClass("AgentEventBusError", { alwaysPermanent: true });
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* @primitive b.agent.eventBus.create
|
|
77
|
+
* @signature b.agent.eventBus.create(opts)
|
|
78
|
+
* @since 0.9.25
|
|
79
|
+
* @status stable
|
|
80
|
+
* @related b.agent.orchestrator.create, b.pubsub.create
|
|
81
|
+
*
|
|
82
|
+
* Create the bus facade. Returns an instance with `registerTopic` /
|
|
83
|
+
* `publish` / `subscribe` / `listTopics`. Operator supplies a pubsub-
|
|
84
|
+
* shaped backend; framework owns schema validation, permission
|
|
85
|
+
* gating, posture re-validation, audit lifecycle.
|
|
86
|
+
*
|
|
87
|
+
* @opts
|
|
88
|
+
* pubsub: { publish, subscribe, unsubscribe }, // required
|
|
89
|
+
* audit: b.audit namespace, // optional
|
|
90
|
+
* permissions: b.permissions instance, // optional
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* var bus = b.agent.eventBus.create({ pubsub: myPubsub });
|
|
94
|
+
* bus.registerTopic("mail.scan.malware-detected", {
|
|
95
|
+
* schema: { source: "string" },
|
|
96
|
+
* });
|
|
97
|
+
* await bus.publish("mail.scan.malware-detected", { source: "1.2.3.4" });
|
|
98
|
+
*/
|
|
99
|
+
function create(opts) {
|
|
100
|
+
if (!opts || typeof opts !== "object") {
|
|
101
|
+
throw new AgentEventBusError("agent-event-bus/bad-opts",
|
|
102
|
+
"create: opts required");
|
|
103
|
+
}
|
|
104
|
+
if (!opts.pubsub || typeof opts.pubsub.publish !== "function" ||
|
|
105
|
+
typeof opts.pubsub.subscribe !== "function") {
|
|
106
|
+
throw new AgentEventBusError("agent-event-bus/bad-pubsub",
|
|
107
|
+
"create: opts.pubsub must expose { publish, subscribe }");
|
|
108
|
+
}
|
|
109
|
+
var auditImpl = opts.audit || audit();
|
|
110
|
+
var permissions = opts.permissions || null;
|
|
111
|
+
var topics = new Map();
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
registerTopic: function (name, topicOpts) { return _registerTopic(topics, name, topicOpts || {}, auditImpl); },
|
|
115
|
+
publish: function (name, payload, pOpts) { return _publish(topics, opts.pubsub, name, payload, pOpts || {}, permissions, auditImpl); },
|
|
116
|
+
subscribe: function (name, handler, sOpts) { return _subscribe(topics, opts.pubsub, name, handler, sOpts || {}, permissions, auditImpl); },
|
|
117
|
+
listTopics: function (args) { return _listTopics(topics, args || {}, permissions); },
|
|
118
|
+
AgentEventBusError: AgentEventBusError,
|
|
119
|
+
guards: {
|
|
120
|
+
topic: guardEventBusTopic,
|
|
121
|
+
payload: guardEventBusPayload,
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ---- Registry -------------------------------------------------------------
|
|
127
|
+
|
|
128
|
+
function _registerTopic(topics, name, topicOpts, auditImpl) {
|
|
129
|
+
guardEventBusTopic.validate(name);
|
|
130
|
+
if (topics.has(name)) {
|
|
131
|
+
throw new AgentEventBusError("agent-event-bus/topic-duplicate",
|
|
132
|
+
"registerTopic: '" + name + "' already registered");
|
|
133
|
+
}
|
|
134
|
+
if (!topicOpts.schema || typeof topicOpts.schema !== "object") {
|
|
135
|
+
throw new AgentEventBusError("agent-event-bus/bad-schema",
|
|
136
|
+
"registerTopic: schema required (flat key→type map)");
|
|
137
|
+
}
|
|
138
|
+
var entry = {
|
|
139
|
+
name: name,
|
|
140
|
+
schema: Object.freeze(Object.assign({}, topicOpts.schema)),
|
|
141
|
+
posture: topicOpts.posture || null,
|
|
142
|
+
tenantScope: topicOpts.tenantScope === true,
|
|
143
|
+
permissions: {
|
|
144
|
+
publish: topicOpts.permissions && Array.isArray(topicOpts.permissions.publish)
|
|
145
|
+
? topicOpts.permissions.publish.slice() : null,
|
|
146
|
+
subscribe: topicOpts.permissions && Array.isArray(topicOpts.permissions.subscribe)
|
|
147
|
+
? topicOpts.permissions.subscribe.slice() : null,
|
|
148
|
+
},
|
|
149
|
+
registeredAt: Date.now(),
|
|
150
|
+
};
|
|
151
|
+
topics.set(name, entry);
|
|
152
|
+
_safeAudit(auditImpl, "agent.event_bus.topic_registered", null, {
|
|
153
|
+
name: name, posture: entry.posture, tenantScope: entry.tenantScope,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function _listTopics(topics, args, permissions) {
|
|
158
|
+
// Permission gate: list-topics requires no special scope by default;
|
|
159
|
+
// operator can wrap with their own permissions instance for stricter.
|
|
160
|
+
var out = [];
|
|
161
|
+
topics.forEach(function (entry) {
|
|
162
|
+
if (args.kind && entry.kind && entry.kind !== args.kind) return;
|
|
163
|
+
out.push({
|
|
164
|
+
name: entry.name,
|
|
165
|
+
schema: entry.schema,
|
|
166
|
+
posture: entry.posture,
|
|
167
|
+
tenantScope: entry.tenantScope,
|
|
168
|
+
registeredAt: entry.registeredAt,
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
return out;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ---- Publish --------------------------------------------------------------
|
|
175
|
+
|
|
176
|
+
async function _publish(topics, pubsub, name, payload, pOpts, permissions, auditImpl) {
|
|
177
|
+
guardEventBusTopic.validate(name);
|
|
178
|
+
var entry = topics.get(name);
|
|
179
|
+
if (!entry) {
|
|
180
|
+
throw new AgentEventBusError("agent-event-bus/unknown-topic",
|
|
181
|
+
"publish: topic '" + name + "' not registered");
|
|
182
|
+
}
|
|
183
|
+
// Permission check for publish.
|
|
184
|
+
if (permissions && entry.permissions.publish) {
|
|
185
|
+
if (!pOpts.actor) {
|
|
186
|
+
throw new AgentEventBusError("agent-event-bus/no-actor",
|
|
187
|
+
"publish: topic '" + name + "' requires actor");
|
|
188
|
+
}
|
|
189
|
+
var allowedPub = false;
|
|
190
|
+
for (var i = 0; i < entry.permissions.publish.length; i += 1) {
|
|
191
|
+
if (permissions.check(pOpts.actor, entry.permissions.publish[i])) {
|
|
192
|
+
allowedPub = true; break;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
if (!allowedPub) {
|
|
196
|
+
_safeAudit(auditImpl, "agent.event_bus.publish_denied", pOpts.actor, { topic: name });
|
|
197
|
+
throw new AgentEventBusError("agent-event-bus/publish-denied",
|
|
198
|
+
"publish: actor lacks any of " + JSON.stringify(entry.permissions.publish) +
|
|
199
|
+
" required for topic '" + name + "'");
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
// Schema validation.
|
|
203
|
+
guardEventBusPayload.validate(payload, entry.schema);
|
|
204
|
+
// Wrap the payload with topic metadata so subscribers can see the
|
|
205
|
+
// posture + tenantScope at delivery (re-validation).
|
|
206
|
+
var wrapped = {
|
|
207
|
+
_topic: name,
|
|
208
|
+
_posture: entry.posture,
|
|
209
|
+
_tenantId: pOpts.actor && pOpts.actor.tenantId ? pOpts.actor.tenantId : null,
|
|
210
|
+
_publishedAt: Date.now(),
|
|
211
|
+
payload: payload,
|
|
212
|
+
};
|
|
213
|
+
await pubsub.publish(name, wrapped);
|
|
214
|
+
_safeAudit(auditImpl, "agent.event_bus.published", pOpts.actor, {
|
|
215
|
+
topic: name, posture: entry.posture,
|
|
216
|
+
});
|
|
217
|
+
return { topic: name, publishedAt: wrapped._publishedAt };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// ---- Subscribe ------------------------------------------------------------
|
|
221
|
+
|
|
222
|
+
async function _subscribe(topics, pubsub, name, handler, sOpts, permissions, auditImpl) {
|
|
223
|
+
guardEventBusTopic.validate(name);
|
|
224
|
+
var entry = topics.get(name);
|
|
225
|
+
if (!entry) {
|
|
226
|
+
throw new AgentEventBusError("agent-event-bus/unknown-topic",
|
|
227
|
+
"subscribe: topic '" + name + "' not registered");
|
|
228
|
+
}
|
|
229
|
+
if (typeof handler !== "function") {
|
|
230
|
+
throw new AgentEventBusError("agent-event-bus/bad-handler",
|
|
231
|
+
"subscribe: handler must be a function");
|
|
232
|
+
}
|
|
233
|
+
// Permission check for subscribe.
|
|
234
|
+
if (permissions && entry.permissions.subscribe) {
|
|
235
|
+
if (!sOpts.actor) {
|
|
236
|
+
throw new AgentEventBusError("agent-event-bus/no-actor",
|
|
237
|
+
"subscribe: topic '" + name + "' requires actor");
|
|
238
|
+
}
|
|
239
|
+
var allowedSub = false;
|
|
240
|
+
for (var i = 0; i < entry.permissions.subscribe.length; i += 1) {
|
|
241
|
+
if (permissions.check(sOpts.actor, entry.permissions.subscribe[i])) {
|
|
242
|
+
allowedSub = true; break;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
if (!allowedSub) {
|
|
246
|
+
_safeAudit(auditImpl, "agent.event_bus.subscribe_denied", sOpts.actor, { topic: name });
|
|
247
|
+
throw new AgentEventBusError("agent-event-bus/subscribe-denied",
|
|
248
|
+
"subscribe: actor lacks any of " + JSON.stringify(entry.permissions.subscribe) +
|
|
249
|
+
" required for topic '" + name + "'");
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
// Cross-tenant subscription gate — when tenantScope is set, the
|
|
253
|
+
// subscriber's actor MUST declare a tenantId at subscribe-time.
|
|
254
|
+
// Subscribers without an actor.tenantId on a tenant-scoped topic
|
|
255
|
+
// are refused outright; the previous shape (filter only when both
|
|
256
|
+
// tenants present) silently accepted such subscribers and let them
|
|
257
|
+
// receive every tenant's events.
|
|
258
|
+
var subscriberTenant = sOpts.actor && sOpts.actor.tenantId ? sOpts.actor.tenantId : null;
|
|
259
|
+
if (entry.tenantScope && !subscriberTenant) {
|
|
260
|
+
_safeAudit(auditImpl, "agent.event_bus.subscribe_denied", sOpts.actor, {
|
|
261
|
+
topic: name, reason: "tenant-scoped-topic-requires-actor-tenant-id",
|
|
262
|
+
});
|
|
263
|
+
throw new AgentEventBusError("agent-event-bus/subscribe-denied",
|
|
264
|
+
"subscribe: tenant-scoped topic '" + name +
|
|
265
|
+
"' requires actor.tenantId; subscribers without a tenant identity are refused");
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Wrapped handler: re-validate posture + tenant at delivery so an
|
|
269
|
+
// in-flight tamper / cross-tenant routing attempt is refused at the
|
|
270
|
+
// consumer boundary (not at the bus's trust boundary alone).
|
|
271
|
+
async function _wrappedHandler(wrapped, evMeta) {
|
|
272
|
+
if (!wrapped || typeof wrapped !== "object" || !wrapped._topic) {
|
|
273
|
+
_safeAudit(auditImpl, "agent.event_bus.delivery_dropped", sOpts.actor,
|
|
274
|
+
{ topic: name, reason: "malformed-envelope" });
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
// Tenant-scope check: subscriber's tenantId must match the
|
|
278
|
+
// publisher's tenantId from the wire envelope. If the envelope
|
|
279
|
+
// lacks _tenantId (publisher omitted), that's a tampered or
|
|
280
|
+
// malformed wire and the delivery drops.
|
|
281
|
+
if (entry.tenantScope) {
|
|
282
|
+
if (!wrapped._tenantId || wrapped._tenantId !== subscriberTenant) {
|
|
283
|
+
_safeAudit(auditImpl, "agent.event_bus.cross_tenant_drop", sOpts.actor, {
|
|
284
|
+
topic: name,
|
|
285
|
+
publisherTenant: wrapped._tenantId || null,
|
|
286
|
+
subscriberTenant: subscriberTenant,
|
|
287
|
+
reason: wrapped._tenantId ? "tenant-mismatch" : "missing-publisher-tenant",
|
|
288
|
+
});
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
// Re-validate payload against schema in case of in-flight tamper.
|
|
293
|
+
try { guardEventBusPayload.validate(wrapped.payload, entry.schema); }
|
|
294
|
+
catch (_e) {
|
|
295
|
+
_safeAudit(auditImpl, "agent.event_bus.delivery_dropped", sOpts.actor,
|
|
296
|
+
{ topic: name, reason: "payload-schema-violation" });
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
// Await handler — supports async handlers + catches their async
|
|
300
|
+
// rejections. Without await, an async handler that rejects would
|
|
301
|
+
// surface as an unhandled rejection and skip the audit emit.
|
|
302
|
+
try {
|
|
303
|
+
await handler(wrapped.payload, {
|
|
304
|
+
topic: name, publishedAt: wrapped._publishedAt,
|
|
305
|
+
source: evMeta && evMeta.source,
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
catch (e) {
|
|
309
|
+
_safeAudit(auditImpl, "agent.event_bus.handler_threw", sOpts.actor,
|
|
310
|
+
{ topic: name, message: (e && e.message) || String(e) });
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
var token = await pubsub.subscribe(name, _wrappedHandler);
|
|
314
|
+
_safeAudit(auditImpl, "agent.event_bus.subscribed", sOpts.actor, { topic: name });
|
|
315
|
+
return function unsubscribe() {
|
|
316
|
+
try {
|
|
317
|
+
if (typeof token === "function") return token();
|
|
318
|
+
if (token && typeof token.unsubscribe === "function") return token.unsubscribe();
|
|
319
|
+
} catch (_e) { /* best-effort */ }
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// ---- Audit helper ---------------------------------------------------------
|
|
324
|
+
|
|
325
|
+
function _safeAudit(auditImpl, action, actor, metadata) {
|
|
326
|
+
agentAudit.safeAudit(auditImpl, action, actor, metadata);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
module.exports = {
|
|
330
|
+
create: create,
|
|
331
|
+
AgentEventBusError: AgentEventBusError,
|
|
332
|
+
guards: {
|
|
333
|
+
topic: guardEventBusTopic,
|
|
334
|
+
payload: guardEventBusPayload,
|
|
335
|
+
},
|
|
336
|
+
};
|
package/lib/agent-idempotency.js
CHANGED
|
@@ -64,6 +64,7 @@ var { defineClass } = require("./framework-error");
|
|
|
64
64
|
var bCrypto = require("./crypto");
|
|
65
65
|
var safeJson = require("./safe-json");
|
|
66
66
|
var guardIdempotencyKey = require("./guard-idempotency-key");
|
|
67
|
+
var agentAudit = require("./agent-audit");
|
|
67
68
|
|
|
68
69
|
var audit = lazyRequire(function () { return require("./audit"); });
|
|
69
70
|
|
|
@@ -331,14 +332,7 @@ function _inMemoryBackend() {
|
|
|
331
332
|
}
|
|
332
333
|
|
|
333
334
|
function _safeAudit(auditImpl, action, actor, metadata) {
|
|
334
|
-
|
|
335
|
-
auditImpl.safeEmit({
|
|
336
|
-
action: action,
|
|
337
|
-
actor: actor ? { id: actor.id, roles: actor.roles || [] } : { id: "<system>" },
|
|
338
|
-
outcome: action.indexOf("denied") >= 0 || action.indexOf("different_args") >= 0 ? "failure" : "success",
|
|
339
|
-
metadata: metadata || {},
|
|
340
|
-
});
|
|
341
|
-
} catch (_e) { /* drop-silent */ }
|
|
335
|
+
agentAudit.safeAudit(auditImpl, action, actor, metadata);
|
|
342
336
|
}
|
|
343
337
|
|
|
344
338
|
module.exports = {
|
|
@@ -56,6 +56,7 @@ var C = require("./constants");
|
|
|
56
56
|
var { defineClass } = require("./framework-error");
|
|
57
57
|
var guardAgentRegistry = require("./guard-agent-registry");
|
|
58
58
|
var bCrypto = require("./crypto");
|
|
59
|
+
var agentAudit = require("./agent-audit");
|
|
59
60
|
|
|
60
61
|
var audit = lazyRequire(function () { return require("./audit"); });
|
|
61
62
|
var cluster = lazyRequire(function () { return require("./cluster"); });
|
|
@@ -449,14 +450,7 @@ function _checkPermission(ctx, actor, scope) {
|
|
|
449
450
|
}
|
|
450
451
|
|
|
451
452
|
function _safeAudit(ctx, action, actor, metadata) {
|
|
452
|
-
|
|
453
|
-
ctx.audit.safeEmit({
|
|
454
|
-
action: action,
|
|
455
|
-
actor: actor ? { id: actor.id, roles: actor.roles || [] } : { id: "<system>" },
|
|
456
|
-
outcome: action.indexOf("denied") >= 0 || action.indexOf("miss") >= 0 ? "failure" : "success",
|
|
457
|
-
metadata: metadata || {},
|
|
458
|
-
});
|
|
459
|
-
} catch (_e) { /* drop-silent — audit emit failures don't crash the call */ }
|
|
453
|
+
agentAudit.safeAudit(ctx.audit, action, actor, metadata);
|
|
460
454
|
}
|
|
461
455
|
|
|
462
456
|
module.exports = {
|