Browse documentation

Capability Audit Events

GocciaScript can report structured events when source reaches a host-controlled capability boundary. Embedders install a sink on the CapabilityAuditSink property of TGocciaEngine; CLI hosts use --audit-log=<path> to write the same events as UTF-8 JSON Lines.

Event Contract

Every event has schema version 1 and these fields:

{
"schemaVersion": 1,
"kind": "net.fetch",
"decision": "deny",
"subject": "blocked.example",
"reason": "the net capability does not allow port 443 of this host",
"source": {
"file": "app.js",
"line": 12,
"column": 5
}
}

decision records the capability decision, not whether the requested operation eventually succeeded. For example, ffi.open reports allow before the dynamic library is loaded; a missing library may still fail afterward. Line and column are null only when an event originates outside an active source call site.

Kinds named after a capability report every decision that consulted it, allow and deny alike:

KindMeaning
capabilities.effectiveThe engine's capability set, as JSON in subject; once per root engine
read.fileA host read that needed the read capability; subject is the canonical path
net.fetchA net decision for a fetch destination: the host name, the address it resolved to, and every redirect hop
net.dispatchAn allowed fetch request reached the dispatch boundary
ffi.openAn ffi decision for a library-open attempt
import.node-modulesAn import decision for a bare specifier resolved against node_modules; one a virtual module, import-map alias, or host module serves never reaches node_modules and emits none
import.providerA provider import decision: the grant for a package, each file fetched, and each cached or loaded file checked against its pin
import.provider.installA change install mode made to a provider pin, or one it refused

The remaining kinds report engine features that are not capabilities:

KindMeaning
function.constructorDynamic Function construction was allowed or denied
shadow-realm.constructAn installed ShadowRealm constructor was invoked
sandbox.fs.pathA sandbox path attempted to cross above the virtual root
config.permissionsA host's trust decision on a config's permission requests; subject is the config path

The CLI emits one config.permissions event per config that requests a grant, before any file runs and before the engines' own events. allow reasons are trusted sha256:<hex>, accepted for this run (-P), and not needed: <Program> honors none of these requests; deny reasons are not trusted, changed since trusted, and ignored (--ignore-config-permissions). A denied config still contributes its deny-* permissions.

net.fetch subjects contain only the checked host. URL user information, paths, and query parameters are not included in host-authorization events. Static imports inside the project are exempt from read (the module graph) and emit nothing.

Disabled FFI and ShadowRealm remain absent globals. Auditing does not install throwing stubs or alter feature detection. Their use events therefore exist only when the host has installed those capabilities.

Sandbox root escapes retain their existing behavior: the escape portion is denied and reported once for the path normalization request, while the resulting in-jail path remains clamped and the filesystem operation continues. Ordinary sandbox reads and writes do not generate events.

capabilities.effective is emitted when a host calls AuditEffectiveCapabilities after installing the sink (the CLI does), or otherwise just before the engine's first other event. Child contexts that inherit the set — ShadowRealm realms and sandbox runScript children — do not repeat it. Its subject is TGocciaCapabilities.ToJSON: one object per layer, each with allowAll, allow, denyAll, and deny for every capability. Its reason is the engine's CapabilityProvenance when the host sets one; the CLI records the command-line grants and the governing config's trust decision, such as cli --allow-read=data; config /repo/goccia.json trusted sha256:<hex>, or config /repo/goccia.json denies only for a config that only denies, or defaults when neither contributes.

import.node-modules subjects are the bare specifier; the reason carries the ceiling the walk was bounded by. See Module Resolution for what the grant permits.

import.provider subjects are the package key (github:<owner>/<repo>@<ref>) for the grant, the derived URL for a fetch, and <package key>/<path> for a pin check; Provider Imports lists the reasons.

Every event is delivered on the runtime thread. The decisions a fetch worker makes for resolved addresses and redirect hops are recorded with the request and delivered when its completion arrives, attributed to the fetch() call that started it. The HTTP worker never calls the sink.

An engine does not wait for requests the script aborted: when Execute returns it discards them, and a worker that reports afterwards has no one to report to. So an abort records a net.fetch event of its own. Its subject is the request's host, as for every net.fetch event (never the URL's path or query), and its reason starts with abandoned: and says that hop decisions reported after the engine ends are not audited. Its decision is allow, because the request was allowed and dispatched; the abandoned: tag is what tells it apart from a destination check. Decisions that do arrive while the engine still runs follow it as usual. The log never silently stops at an aborted request's net.dispatch.

A request an embedder's fetch transport answers makes no hop decisions the engine sees: its events are the net.dispatch and the net.fetch name check fetch() records before handing it over, and an abort adds no abandoned: event.

Embedding

The sink is a Pascal method callback:

uses
Goccia.CapabilityAudit,
Goccia.Engine;
​
procedure TMyHost.HandleCapabilityAudit(
const AEvent: TGocciaCapabilityAuditEvent);
begin
WriteLn(AEvent.ToJSON);
end;
​
Engine.CapabilityAuditSink := HandleCapabilityAudit;

Install the sink before executing source or attaching runtime capabilities. ShadowRealm child engines inherit the parent engine's sink.

Sink exceptions propagate as EGocciaCapabilityAuditDeliveryError and stop execution. Script-level try/catch and promise rejection handlers cannot intercept delivery failures. Hosts must treat delivery as part of the security contract: serialize concurrent calls when engines run on multiple threads, and do not swallow storage or transport failures.

CLI

All TGocciaCLIApplication-based hosts accept:

./build/GocciaRunner app.js --audit-log=capabilities.jsonl
./build/GocciaRunner main.js --copy data \
--audit-log=capabilities.jsonl

The file is created before execution. Failure to open it or write an event is fatal. Writes are serialized, so batch hosts may safely combine events from --jobs=N workers into one JSONL stream. --log and --audit-log must name different files; the CLI rejects equivalent expanded paths before opening either output.