R6 lifecycle owner for protected datasets, deterministic value indexes,
strategy configuration, tool wrapping, and strict DescribeData metadata.
Create one instance per Shiny session or thread; explicitly share an instance
only when those chat threads intentionally share the same protected data.
codeagent_client(data_shield = list(shield_*())) is the declarative
convenience path and creates a private DataShield internally. Pass an
explicit DataShield instance when data must be registered dynamically or
shared across chats. Instances are intentionally non-cloneable; create a new
object for an independent user/thread boundary.
Methods
DataShield$new()
Create a Data Shield.
Usage
DataShield$new(
max_rows = 0L,
distributions = "off",
k_anon = 5L,
category_max = 20L,
category_ratio = 0.2,
audit_max = 1000L,
dp_epsilon = .DATA_SHIELD_DP_EPSILON_DEFAULT,
dp_budget = .DATA_SHIELD_DP_BUDGET_DEFAULT,
strategies = NULL
)Arguments
max_rowsDirect
row_capvalue whenstrategies = NULL:0exposes no raw tabular line; positive values retain that many leading printed lines.distributionsDirect DescribeData policy.
"off"(default, safe): labels only."on"/"dp": real/DP-noised per-category counts (seeshield_describe()); numeric/date columns unchanged in all modes.k_anonMinimum category support for exposing a label.
category_maxMaximum distinct character values treated as a category.
category_ratioMaximum distinct/non-missing ratio for character categorical treatment.
audit_maxMaximum in-memory non-sensitive decision events retained.
dp_epsilonDirect per-column-per-call DP privacy cost when
distributions = "dp"(seeshield_describe()).dp_budgetDirect per-dataset total DP privacy budget when
distributions = "dp"(seeshield_describe()).strategiesOptional ordered list from
shield_describe(),shield_egress(),shield_regex(),shield_ingress(),shield_tool_policy(),shield_sandbox(), andshield_reviewer(). If supplied, only listed strategies are enabled and list order controls egress execution order.
DataShield$register_data()
Register one protected data.frame.
Usage
DataShield$register_data(
df,
name = NULL,
sensitivity = NULL,
cols = NULL,
min_len = 3L,
min_card = 8L,
max_index_values = 500000L,
column_access = NULL
)Arguments
dfA data.frame retained locally; rows are never emitted by
DescribeData.nameDataset name used by the model-facing
DescribeDatatool.sensitivityOptional named overrides:
identifier,quasi,measure, oropen. Local heuristics classify unspecified columns.colsOptional explicit value-match columns. Default: columns classified
identifier/quasi.min_len, min_cardMinimum value length and column cardinality for deterministic value indexing (reduces low-entropy false positives).
max_index_valuesCap on indexed values (default 500000, ~65MB of keys). On overflow, indexing stops and a warning is emitted; unindexed values are not caught by value_match and rely on the other egress layers.
NULL/Infdisables the cap.column_accessOptional named list of per-column raw-access overrides, each
list(prompt=, egress=, reason=, scan_secrets=)usingnone/schema/scan/raw. A raw edge requires a non-emptyreason; overrides missing it are dropped (with a warning) so the column falls back to its sensitivity tier.egress="raw"removes the column from the value-match index;prompt="raw"letsDescribeDataenumerate its real values.
DataShield$register_asset()
Register a typed data/document/spec asset and its LLM access policy.
Usage
DataShield$register_asset(
x,
name,
kind,
llm_access = NULL,
scan_secrets = TRUE,
reason = NULL,
expires = "session"
)Arguments
xLocal asset value or path.
nameUnique asset name.
kinddataset,spec,document, orsynthetic.llm_accessNULL for kind defaults, or list(prompt=, egress=) using
none,schema,scan, orraw.scan_secretsKeep baseline PII/secret regex active for raw access.
reasonRequired when prompt or egress access is raw.
expires"session"or POSIXct expiry.
DataShield$schema_block()
Build a system-prompt block listing every registered
protected dataset with its filtered schema (the same per-dataset output
DescribeData produces). Reused by the system-prompt builder so the
model knows what protected data exists without calling the tool first.
Reads live engine state, so it reflects the current dataset set (grows
as register_data is called). Returns "" when no dataset is
registered or DescribeData is disabled.
DataShield$dp_budget_remaining()
Return remaining DP privacy budget (epsilon units) for a
registered dataset, or a named vector for all datasets when name is
NULL. NA_real_ for a dataset not under distributions="dp".
DataShield$scan_egress()
Apply the ordered egress strategy pipeline to a tool result.
Usage
DataShield$scan_egress(result, context = list())DataShield$scan_ingress()
Scan one tool request before execution.
DataShield$scan_tool_args()
Redact protected values inside a tool's arguments before the
tool executes (ingress rewrite). Complements scan_ingress (which
decides pass/block/ask): this scrubs each string argument value in place
using the same detectors as scan_prompt (value_match + PII regex), so
a registered value pasted into a tool argument is redacted rather than
the whole call being blocked. Runs in the tool wrapper, after the
permission gate. Non-string arguments are left untouched.
Usage
DataShield$scan_tool_args(args, scanners = c("regex", "value_match"))DataShield$scan_prompt()
Scan a user prompt BEFORE it reaches the model (edge 1). This is the Data Shield half of the prompt gate: it detects protected data the user may have pasted into their message. Unlike egress (which withholds a whole unsafe tool result), prompt redaction replaces ONLY the matched values / PII spans and keeps the rest of the user's text – the user's original wording is otherwise preserved.
Two detectors, both reusing existing machinery:
value_match: does the prompt contain a REGISTERED protected value (e.g. a real USUBJID)? O(1) hash lookup via the value index.
regex/PII: email / phone / token / id shapes.
Arguments
textCharacter scalar. The raw user prompt.
on_fail"redact"(default, replace matches, keep rest),"block"(reject the whole turn), or"ask"(defer to approval).on_progressOptional
function(list(stage, status, matched, elapsed_ms))progress callback so a UI can show "scanning data safety...". NULL (default) is silent and zero-overhead.contextOptional non-sensitive context (e.g.
tool_call_id,edge).context$edgelabels audit events; defaults to"prompt"so the reusable output-side wrapper (scan_response) can passedge = "response"to distinguish direction in the audit log.scannersCharacter vector selecting which detectors run, a subset of
c("regex", "value_match"). Default runs both (secure-by-default); a host may drop one viasettings$data_shield_input_scanners/data_shield_output_scanners.
DataShield$scan_response()
Scan the model's final reply BEFORE it reaches the user
(edge 3, the output gate). Symmetric to scan_prompt (edge 1): the
model may reproduce a protected value it inferred from tool output even
when the user's input was clean, so the reply is scanned on the way out.
A thin wrapper over scan_prompt – identical detectors (value_match +
PII regex), differing only in the audit edge label ("response").
DataShield$review_code_public()
Public bridge to the internal code reviewer rail (kiro
round-2 #7). The reviewer logic lives in private$review_code, so an
external caller (the AuditCode pipeline, .audit_code_impl) could not
reach it – shield$review_code resolved to NULL and every audit fell
back to "reviewer unavailable". This public method exposes the rail so
the deterministic audit can feed vetted, whitelisted file contents to
the configured reviewer. Returns the reviewer verdict (list with
risk/error) or a promise thereof; list(error=TRUE, reason=...)
when no reviewer is configured.
Usage
DataShield$review_code_public(text, context = list())Examples
# Easy one-client declaration:
specs <- list(
shield_describe(k_anon = 5),
shield_egress(max_rows = 0),
shield_regex(on_fail = "redact")
)
# Explicit lifecycle for uploaded data / selected shared chats:
shield <- DataShield$new(strategies = specs)
shield$register_data(iris, name = "iris",
sensitivity = c(Species = "measure"))
shield$coverage()
#> $config
#> $config$max_rows
#> [1] 0
#>
#> $config$distributions
#> [1] "off"
#>
#> $config$k_anon
#> [1] 5
#>
#> $config$category_max
#> [1] 20
#>
#> $config$category_ratio
#> [1] 0.2
#>
#> $config$dp_epsilon
#> [1] 1
#>
#> $config$dp_budget
#> [1] 5
#>
#> $config$detectors
#> [1] "row_cap" "value_match"
#>
#> $config$on_fail
#> [1] "redact"
#>
#> $config$allow_raw_approval
#> [1] FALSE
#>
#> $config$approval_timeout
#> [1] 60
#>
#> $config$describe_enabled
#> [1] TRUE
#>
#> $config$egress_enabled
#> [1] TRUE
#>
#>
#> $datasets
#> [1] "iris"
#>
#> $assets
#> NULL
#>
#> $indexed_values
#> [1] 0
#>
#> $raw_access_columns
#> [1] 0
#>
#> $egress_pipeline
#> [1] "egress" "regex"
#>
#> $ingress_pipeline
#> character(0)
#>
#> $audit_events
#> [1] 0
#>
#> $audit_max
#> [1] 1000
#>
#> $egress_approval_callback
#> [1] FALSE
#>
#> $tool_policy_default
#> [1] "scan"
#>
#> $tool_policy_rules
#> NULL
#>
#> $sandbox
#> NULL
#>
#> $reviewers
#> [1] 0
#>
#> $reviewer_factory_bound
#> [1] FALSE
#>
#> $closed
#> [1] FALSE
#>