beatrina 0.8.6
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 +92 -0
- package/NOTICES +72 -0
- package/README.md +124 -0
- package/bin/beatrina.mjs +223 -0
- package/bin/cli.mjs +80 -0
- package/bin/failsafe.mjs +74 -0
- package/bin/identity.mjs +45 -0
- package/bin/prova-post.mjs +51 -0
- package/bin/sessions.mjs +95 -0
- package/bin/shortcut.mjs +151 -0
- package/bin/update-check.mjs +55 -0
- package/bin/upgrade.mjs +76 -0
- package/build-info.json +1 -0
- package/carmar_V0.8.6.html +1310 -0
- package/check/acceptance.mjs +278 -0
- package/check/session.mjs +215 -0
- package/engines/js/document-exec.mjs +82 -0
- package/engines/js/persist.mjs +214 -0
- package/engines/js/worker.mjs +424 -0
- package/engines/python/adapter.py +577 -0
- package/engines/python/analyze.py +814 -0
- package/engines/python/bootstrap.py +309 -0
- package/engines/python/dataview.py +735 -0
- package/engines/python/debugger.py +346 -0
- package/engines/python/document_exec.py +158 -0
- package/engines/python/engine.json +28 -0
- package/engines/python/handoff.py +118 -0
- package/engines/python/worker.py +564 -0
- package/engines/r/engine.json +25 -0
- package/engines/r/handoff.R +92 -0
- package/failsafe/ai-policy.R +255 -0
- package/failsafe/ai-store.R +373 -0
- package/failsafe/cite.R +418 -0
- package/failsafe/journal.R +684 -0
- package/failsafe/plugins.R +809 -0
- package/failsafe/serve.R +5500 -0
- package/host/ai-policy.mjs +218 -0
- package/host/deployment.mjs +160 -0
- package/host/engine-js.mjs +98 -0
- package/host/engine-pool.mjs +383 -0
- package/host/engine-python.mjs +228 -0
- package/host/engine-r.mjs +206 -0
- package/host/engine-stdio.mjs +401 -0
- package/host/journal-store.mjs +749 -0
- package/host/main.mjs +503 -0
- package/host/planes/README.md +41 -0
- package/host/planes/ai-store.mjs +327 -0
- package/host/planes/ai.mjs +467 -0
- package/host/planes/analyze.mjs +397 -0
- package/host/planes/cite.mjs +517 -0
- package/host/planes/files.mjs +0 -0
- package/host/planes/jobs.mjs +704 -0
- package/host/planes/journal.mjs +53 -0
- package/host/planes/latex.mjs +201 -0
- package/host/planes/mcp.mjs +493 -0
- package/host/planes/pair.mjs +325 -0
- package/host/planes/pipe-term.mjs +122 -0
- package/host/planes/plugins.mjs +112 -0
- package/host/planes/proc-tree.mjs +76 -0
- package/host/planes/sessions.mjs +434 -0
- package/host/planes/settings.mjs +164 -0
- package/host/planes/terminal.mjs +286 -0
- package/host/planes/test-file.mjs +80 -0
- package/host/planes/update.mjs +214 -0
- package/host/plugin-store.mjs +838 -0
- package/host/server.mjs +441 -0
- package/host/settings.mjs +379 -0
- package/host/update-record.mjs +59 -0
- package/host/user-dirs.mjs +117 -0
- package/host/windows-runtime.mjs +145 -0
- package/host/worker-plane.mjs +713 -0
- package/host/ws.mjs +190 -0
- package/kernel/analyze.R +668 -0
- package/kernel/deployment.R +165 -0
- package/kernel/examples/NOTICE.md +38 -0
- package/kernel/examples/tna-complete-tutorial.Rmd +210 -0
- package/kernel/fileio.R +656 -0
- package/kernel/index.html +96 -0
- package/kernel/job-run.R +391 -0
- package/kernel/jobs.R +276 -0
- package/kernel/kernel-protocol +1 -0
- package/kernel/kernel-version +1 -0
- package/kernel/kernel.R +671 -0
- package/kernel/knitr-run.R +245 -0
- package/kernel/latex.R +609 -0
- package/kernel/mcp/carmar-mcp.mjs +516 -0
- package/kernel/notebook-page.R +67 -0
- package/kernel/plugins/csl/apa/apa.csl +2273 -0
- package/kernel/plugins/csl/apa/plugin.json +19 -0
- package/kernel/plugins/csl/chicago-author-date/chicago-author-date.csl +4216 -0
- package/kernel/plugins/csl/chicago-author-date/plugin.json +19 -0
- package/kernel/plugins/csl/harvard-cite-them-right/harvard-cite-them-right.csl +316 -0
- package/kernel/plugins/csl/harvard-cite-them-right/plugin.json +19 -0
- package/kernel/plugins/csl/ieee/ieee.csl +519 -0
- package/kernel/plugins/csl/ieee/plugin.json +19 -0
- package/kernel/plugins/csl/modern-language-association/modern-language-association.csl +1184 -0
- package/kernel/plugins/csl/modern-language-association/plugin.json +19 -0
- package/kernel/plugins/csl/nature/nature.csl +189 -0
- package/kernel/plugins/csl/nature/plugin.json +19 -0
- package/kernel/plugins/latex/apa7/apa7.json +14 -0
- package/kernel/plugins/latex/apa7/plugin.json +19 -0
- package/kernel/plugins/latex/elsarticle/elsarticle.json +14 -0
- package/kernel/plugins/latex/elsarticle/plugin.json +19 -0
- package/kernel/plugins/latex/ieeetran/ieeetran.json +10 -0
- package/kernel/plugins/latex/ieeetran/plugin.json +19 -0
- package/kernel/project.R +131 -0
- package/kernel/settings.R +410 -0
- package/kernel/sniff.R +769 -0
- package/kernel/worker-boot.R +22 -0
- package/kernel/worker.R +3496 -0
- package/lib/agent-authoring-contract.js +547 -0
- package/lib/cell-kinds.js +108 -0
- package/lib/engine-labels.js +324 -0
- package/package.json +32 -0
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
# settings.R — the settings a USER may change, and the ones only an
|
|
2
|
+
# administrator may.
|
|
3
|
+
#
|
|
4
|
+
# Until now every kernel-side preference was an environment variable read once
|
|
5
|
+
# at startup: CARMAR_LINGER, CARMAR_NO_HISTORY, CARMAR_ROOT, CARMAR_LOG. That
|
|
6
|
+
# is exactly right for a deployment and useless for a person, who has no place
|
|
7
|
+
# to set one and no way to see what it currently is. This module adds a user
|
|
8
|
+
# config file and — more importantly — the rule for who wins.
|
|
9
|
+
#
|
|
10
|
+
# Pure, in the shape of deployment.R and ai-policy.R: `env` is injectable, the
|
|
11
|
+
# resolve returns a whole decision rather than mutating anything, and the only
|
|
12
|
+
# I/O is the one file. serve.R sources it; nothing else does.
|
|
13
|
+
#
|
|
14
|
+
# ── THE PRECEDENCE RULE ─────────────────────────────────────────────────────
|
|
15
|
+
#
|
|
16
|
+
# environment variable (set and non-empty) ▸ config file ▸ built-in default
|
|
17
|
+
#
|
|
18
|
+
# and two consequences that matter more than the ordering:
|
|
19
|
+
#
|
|
20
|
+
# · A setting whose env var is set is READ-ONLY for the life of this kernel.
|
|
21
|
+
# Not "the file is ignored this time" — `settings_set` refuses it and does
|
|
22
|
+
# not write the file, because a value silently stored and never applied is
|
|
23
|
+
# a worse lie than a refusal.
|
|
24
|
+
#
|
|
25
|
+
# · An administrator's setting is NOT IN THIS FILE'S VOCABULARY AT ALL. It is
|
|
26
|
+
# not "env wins if present": the key is rejected on read and refused on
|
|
27
|
+
# write, always. "Env wins when set" only guarantees anything WHEN THE ENV
|
|
28
|
+
# IS SET, and a deployment that never sets CARMAR_ROOT is still one where a
|
|
29
|
+
# user must not invent one — a root the user chose reads as protection
|
|
30
|
+
# while being none (worker.R is explicit that it is a guardrail, not a
|
|
31
|
+
# sandbox). The refusal must not depend on an administrator having
|
|
32
|
+
# remembered to set every variable defensively.
|
|
33
|
+
#
|
|
34
|
+
# The tempting alternative — a layered Sys.getenv-shaped getter handed to
|
|
35
|
+
# carmar_deployment() and carmar_ai_policy() — is deliberately NOT built. It
|
|
36
|
+
# would put every admin variable one argument-passing mistake away from being
|
|
37
|
+
# file-settable. Those two keep plain Sys.getenv; only the user-writable call
|
|
38
|
+
# sites in serve.R read this module.
|
|
39
|
+
|
|
40
|
+
SETTINGS_VERSION <- 1L
|
|
41
|
+
|
|
42
|
+
# The whole vocabulary. A key that is not here does not exist: there is no
|
|
43
|
+
# pass-through, so the file cannot grow a control by accident.
|
|
44
|
+
#
|
|
45
|
+
# `effect` is a FIELD, not a sentence the page composes, so a setting added
|
|
46
|
+
# later cannot arrive without an honest answer to "when does this take
|
|
47
|
+
# effect?". Its three values:
|
|
48
|
+
# live the running kernel changes now
|
|
49
|
+
# restart_r the next R session picks it up (Session ▸ Restart R)
|
|
50
|
+
# next_launch only a new kernel
|
|
51
|
+
SETTINGS <- list(
|
|
52
|
+
# Connection loss is not permission to discard the R workspace. Timed
|
|
53
|
+
# shutdown is opt-in; 0 retains an attached session until explicit Quit.
|
|
54
|
+
list(key = "linger_seconds", env = "CARMAR_LINGER", kind = "number",
|
|
55
|
+
default = 0, min = 0, max = 86400, effect = "live",
|
|
56
|
+
label = "Keep R running after all pages disconnect"),
|
|
57
|
+
list(key = "history_enabled", env = "CARMAR_NO_HISTORY", kind = "bool",
|
|
58
|
+
default = TRUE, effect = "live", inverted = TRUE,
|
|
59
|
+
label = "Remember console history between sessions"),
|
|
60
|
+
list(key = "analyze_budget_s", env = "CARMAR_ANALYZE_BUDGET", kind = "number",
|
|
61
|
+
default = 3, min = 0, max = 60, effect = "next_launch",
|
|
62
|
+
label = "Code-intelligence time budget"),
|
|
63
|
+
list(key = "quarto_path", env = "CARMAR_QUARTO", kind = "path",
|
|
64
|
+
default = "", effect = "live",
|
|
65
|
+
label = "Quarto binary"),
|
|
66
|
+
list(key = "rscript_path", env = "CARMAR_RSCRIPT", kind = "path",
|
|
67
|
+
default = "", effect = "restart_r",
|
|
68
|
+
label = "R binary"),
|
|
69
|
+
list(key = "port", env = "CARMAR_PORT", kind = "number",
|
|
70
|
+
default = 4747, min = 1024, max = 65535, effect = "next_launch",
|
|
71
|
+
label = "Preferred port")
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
# Reported to the page as read-only facts, never settable. Everything an
|
|
75
|
+
# administrator owns lives here and NOWHERE in SETTINGS, which is what makes
|
|
76
|
+
# the refusal structural rather than a lookup someone can forget.
|
|
77
|
+
SETTINGS_ADMIN <- c(
|
|
78
|
+
"CARMAR_MANAGED_CONFIG",
|
|
79
|
+
"CARMAR_ROOT", "CARMAR_LOG", "CARMAR_LOG_AI_TEXT",
|
|
80
|
+
"CARMAR_CRAN_MIRROR",
|
|
81
|
+
"CARMAR_AI_LOCAL_ONLY", "CARMAR_AI_POLICY", "CARMAR_AI_PROVIDERS",
|
|
82
|
+
"CARMAR_BIND", "CARMAR_HOSTS", "CARMAR_ORIGINS", "CARMAR_REQUIRE_ORIGIN",
|
|
83
|
+
"CARMAR_TRUST_PROXY", "CARMAR_USER_HEADER", "CARMAR_TRUSTED_PROXY",
|
|
84
|
+
"CARMAR_ALLOW_UNAUTHENTICATED", "CARMAR_PORT_STRICT",
|
|
85
|
+
"CARMAR_UPDATE_FEED", "CARMAR_UPDATE_MIRROR", "CARMAR_UPDATE_OFFLINE_DIR",
|
|
86
|
+
"CARMAR_UPDATE_MANAGED"
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
# A managed desktop cannot depend on shell startup files: Finder/LaunchServices
|
|
90
|
+
# does not read them, and a standard user can edit them. Jamf/Intune instead
|
|
91
|
+
# place this small JSON file in an administrator-owned directory. Its values
|
|
92
|
+
# become environment variables before deployment, AI, update, or user-setting
|
|
93
|
+
# policy is resolved, so the existing env-wins-and-locks rule remains the one
|
|
94
|
+
# source of truth.
|
|
95
|
+
MANAGED_SETTINGS_SCHEMA <- "carmar-managed-settings-v1"
|
|
96
|
+
MANAGED_SETTINGS_ENV <- unique(c(
|
|
97
|
+
vapply(SETTINGS, function(s) s$env, character(1)),
|
|
98
|
+
setdiff(SETTINGS_ADMIN, "CARMAR_MANAGED_CONFIG"),
|
|
99
|
+
"HTTP_PROXY", "HTTPS_PROXY", "NO_PROXY"
|
|
100
|
+
))
|
|
101
|
+
|
|
102
|
+
carmar_managed_default_path <- function(env = Sys.getenv,
|
|
103
|
+
os = .Platform$OS.type,
|
|
104
|
+
sysname = Sys.info()[["sysname"]]) {
|
|
105
|
+
if (identical(sysname, "Darwin")) {
|
|
106
|
+
return("/Library/Application Support/CarmaR/managed-settings.json")
|
|
107
|
+
}
|
|
108
|
+
if (identical(os, "windows")) {
|
|
109
|
+
base <- trimws(env("ProgramData", ""))
|
|
110
|
+
if (!nzchar(base)) base <- "C:/ProgramData"
|
|
111
|
+
return(file.path(base, "CarmaR", "managed-settings.json"))
|
|
112
|
+
}
|
|
113
|
+
"/etc/carmar/managed-settings.json"
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
# Protection is checked from the standard user's point of view. Unix also
|
|
117
|
+
# requires root ownership because a user-owned 0400 file can simply be chmod'd
|
|
118
|
+
# and replaced. On Windows, the deployment recipe gives ProgramData/CarmaR an
|
|
119
|
+
# Administrators+SYSTEM write ACL; neither the file nor its parent may be
|
|
120
|
+
# writable by the running account.
|
|
121
|
+
carmar_managed_protected <- function(path, os = .Platform$OS.type) {
|
|
122
|
+
if (nzchar(Sys.readlink(path))) return(FALSE)
|
|
123
|
+
if (file.access(path, 2L) == 0L || file.access(dirname(path), 2L) == 0L) {
|
|
124
|
+
return(FALSE)
|
|
125
|
+
}
|
|
126
|
+
if (!identical(os, "windows")) {
|
|
127
|
+
info <- file.info(path)
|
|
128
|
+
if (is.na(info$uid[[1]]) || info$uid[[1]] != 0L || settings_insecure(path)) {
|
|
129
|
+
return(FALSE)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
TRUE
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
# Read one administrator policy atomically. Unlike the user settings file,
|
|
136
|
+
# unknown or malformed entries fail the WHOLE policy closed: partial policy is
|
|
137
|
+
# not a safe enterprise fallback.
|
|
138
|
+
carmar_managed_read <- function(path, required = FALSE,
|
|
139
|
+
protection = carmar_managed_protected) {
|
|
140
|
+
empty <- list(status = "missing", path = path, values = character(), errors = character())
|
|
141
|
+
if (!file.exists(path)) {
|
|
142
|
+
if (isTRUE(required)) {
|
|
143
|
+
empty$status <- "invalid"
|
|
144
|
+
empty$errors <- "CARMAR_MANAGED_CONFIG names a file that does not exist."
|
|
145
|
+
}
|
|
146
|
+
return(empty)
|
|
147
|
+
}
|
|
148
|
+
if (!isTRUE(protection(path))) {
|
|
149
|
+
empty$status <- "insecure"
|
|
150
|
+
empty$errors <- paste0(
|
|
151
|
+
"The managed settings file is writable by the current account, is a symlink, ",
|
|
152
|
+
"or is not administrator-owned; CarmaR refused it.")
|
|
153
|
+
return(empty)
|
|
154
|
+
}
|
|
155
|
+
size <- file.info(path)$size[[1]]
|
|
156
|
+
if (is.na(size) || size > 1024 * 1024) {
|
|
157
|
+
empty$status <- "invalid"
|
|
158
|
+
empty$errors <- "The managed settings file exceeds the 1 MB limit."
|
|
159
|
+
return(empty)
|
|
160
|
+
}
|
|
161
|
+
raw <- tryCatch(jsonlite::fromJSON(path, simplifyVector = FALSE),
|
|
162
|
+
error = function(e) NULL)
|
|
163
|
+
root_names <- if (is.list(raw)) names(raw) else NULL
|
|
164
|
+
if (is.null(root_names) || anyDuplicated(root_names) ||
|
|
165
|
+
!setequal(root_names, c("schema", "settings")) ||
|
|
166
|
+
!identical(raw$schema, MANAGED_SETTINGS_SCHEMA) ||
|
|
167
|
+
!is.list(raw$settings) || is.null(names(raw$settings)) ||
|
|
168
|
+
anyDuplicated(names(raw$settings))) {
|
|
169
|
+
empty$status <- "invalid"
|
|
170
|
+
empty$errors <- paste0("The managed settings file must use schema ",
|
|
171
|
+
MANAGED_SETTINGS_SCHEMA, " with one settings object.")
|
|
172
|
+
return(empty)
|
|
173
|
+
}
|
|
174
|
+
keys <- names(raw$settings)
|
|
175
|
+
unknown <- setdiff(keys, MANAGED_SETTINGS_ENV)
|
|
176
|
+
bad <- keys[!vapply(raw$settings, function(value) {
|
|
177
|
+
is.character(value) && length(value) == 1L && !is.na(value) &&
|
|
178
|
+
nzchar(value) && !grepl("[\r\n\t]", value)
|
|
179
|
+
}, logical(1))]
|
|
180
|
+
errors <- character()
|
|
181
|
+
if (length(unknown)) {
|
|
182
|
+
errors <- c(errors, paste0("Unknown managed setting: ", paste(unknown, collapse = ", "), "."))
|
|
183
|
+
}
|
|
184
|
+
if (length(bad)) {
|
|
185
|
+
errors <- c(errors, paste0("Managed settings must be non-empty single-line strings: ",
|
|
186
|
+
paste(bad, collapse = ", "), "."))
|
|
187
|
+
}
|
|
188
|
+
if (length(errors)) {
|
|
189
|
+
empty$status <- "invalid"
|
|
190
|
+
empty$errors <- errors
|
|
191
|
+
return(empty)
|
|
192
|
+
}
|
|
193
|
+
values <- vapply(raw$settings, identity, character(1))
|
|
194
|
+
list(status = "ok", path = path, values = values, errors = character())
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
carmar_managed_environment <- function(env = Sys.getenv, path = NULL,
|
|
198
|
+
protection = carmar_managed_protected,
|
|
199
|
+
apply_values = TRUE) {
|
|
200
|
+
explicit <- trimws(env("CARMAR_MANAGED_CONFIG", ""))
|
|
201
|
+
if (is.null(path)) path <- if (nzchar(explicit)) explicit else carmar_managed_default_path(env)
|
|
202
|
+
result <- carmar_managed_read(path, required = nzchar(explicit), protection = protection)
|
|
203
|
+
if (isTRUE(apply_values) && identical(result$status, "ok") && length(result$values)) {
|
|
204
|
+
do.call(Sys.setenv, as.list(result$values))
|
|
205
|
+
}
|
|
206
|
+
result
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
settings_entry <- function(key) {
|
|
210
|
+
for (s in SETTINGS) if (identical(s$key, key)) return(s)
|
|
211
|
+
NULL
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
#' Where the user's settings live — beside the AI key, same directory.
|
|
215
|
+
carmar_settings_path <- function(env = Sys.getenv) {
|
|
216
|
+
dir <- tools::R_user_dir("carmar", "config")
|
|
217
|
+
dir.create(dir, recursive = TRUE, showWarnings = FALSE)
|
|
218
|
+
file.path(dir, "settings.json")
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
#' Is the file writable by anyone but its owner?
|
|
222
|
+
#'
|
|
223
|
+
#' This file is an INTEGRITY surface rather than a secret: another local account
|
|
224
|
+
#' that can write it turns a knob in this user's next kernel. So a
|
|
225
|
+
#' group- or other-writable file is IGNORED rather than repaired — silently
|
|
226
|
+
#' chmod-ing someone else's file back to 0600 would hide that it had been
|
|
227
|
+
#' changed at all.
|
|
228
|
+
settings_insecure <- function(path) {
|
|
229
|
+
info <- file.info(path)
|
|
230
|
+
if (is.na(info$mode[[1]])) return(FALSE)
|
|
231
|
+
mode <- as.integer(info$mode[[1]])
|
|
232
|
+
bitwAnd(mode, as.integer(strtoi("077", 8L))) != 0L
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
#' Validate one value against its entry.
|
|
236
|
+
#' @return list(ok, value, reason)
|
|
237
|
+
carmar_settings_validate <- function(key, value) {
|
|
238
|
+
entry <- settings_entry(key)
|
|
239
|
+
if (is.null(entry)) {
|
|
240
|
+
if (key %in% SETTINGS_ADMIN) {
|
|
241
|
+
return(list(ok = FALSE, reason = "administrator setting"))
|
|
242
|
+
}
|
|
243
|
+
return(list(ok = FALSE, reason = "unknown setting"))
|
|
244
|
+
}
|
|
245
|
+
if (identical(entry$kind, "bool")) {
|
|
246
|
+
if (!is.logical(value) || length(value) != 1L || is.na(value)) {
|
|
247
|
+
return(list(ok = FALSE, reason = "must be true or false"))
|
|
248
|
+
}
|
|
249
|
+
return(list(ok = TRUE, value = value))
|
|
250
|
+
}
|
|
251
|
+
if (identical(entry$kind, "number")) {
|
|
252
|
+
if (!is.numeric(value) || length(value) != 1L || !is.finite(value)) {
|
|
253
|
+
return(list(ok = FALSE, reason = "must be a number"))
|
|
254
|
+
}
|
|
255
|
+
if (!is.null(entry$min) && value < entry$min) {
|
|
256
|
+
return(list(ok = FALSE, reason = paste0("must be at least ", entry$min)))
|
|
257
|
+
}
|
|
258
|
+
if (!is.null(entry$max) && value > entry$max) {
|
|
259
|
+
return(list(ok = FALSE, reason = paste0("must be at most ", entry$max)))
|
|
260
|
+
}
|
|
261
|
+
# Always a double. jsonlite reads 900 from a file as an INTEGER and 900.0
|
|
262
|
+
# from a set frame as a double, so without this the same setting has two
|
|
263
|
+
# types depending on how it arrived — and every `identical()` against it
|
|
264
|
+
# becomes a coin toss.
|
|
265
|
+
return(list(ok = TRUE, value = as.numeric(value)))
|
|
266
|
+
}
|
|
267
|
+
# path
|
|
268
|
+
if (!is.character(value) || length(value) != 1L || is.na(value)) {
|
|
269
|
+
return(list(ok = FALSE, reason = "must be a string"))
|
|
270
|
+
}
|
|
271
|
+
# An empty path means "use the ladder" — that is a legitimate value, not a
|
|
272
|
+
# missing one, and it is how a user clears an override.
|
|
273
|
+
if (nzchar(value) && !file.exists(value)) {
|
|
274
|
+
return(list(ok = FALSE, reason = "no such file"))
|
|
275
|
+
}
|
|
276
|
+
list(ok = TRUE, value = value)
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
#' Read the config file.
|
|
280
|
+
#'
|
|
281
|
+
#' FOUR states, kept distinct — the ai-policy.R lesson that folding failure
|
|
282
|
+
#' modes together is the fail-open bug:
|
|
283
|
+
#' missing no file. The ordinary single-user state, NOT an error.
|
|
284
|
+
#' ok parsed, an object, the right version.
|
|
285
|
+
#' invalid unparseable, not an object, wrong version → all defaults.
|
|
286
|
+
#' insecure group/other writable → all defaults, different words.
|
|
287
|
+
#'
|
|
288
|
+
#' Per-key failure stays per-key: an unknown key joins `ignored`, a bad value
|
|
289
|
+
#' joins `rejected`, and the rest of the file still applies. One bad number
|
|
290
|
+
#' must not silently revert eleven good ones.
|
|
291
|
+
carmar_settings_read <- function(path = carmar_settings_path()) {
|
|
292
|
+
empty <- list(status = "missing", path = path, values = list(),
|
|
293
|
+
ignored = character(), rejected = list(), error = "")
|
|
294
|
+
if (!file.exists(path)) return(empty)
|
|
295
|
+
if (settings_insecure(path)) {
|
|
296
|
+
return(modifyList(empty, list(status = "insecure",
|
|
297
|
+
error = "The settings file is writable by other accounts on this machine, so it was ignored.")))
|
|
298
|
+
}
|
|
299
|
+
raw <- tryCatch(jsonlite::fromJSON(path, simplifyVector = FALSE),
|
|
300
|
+
error = function(e) NULL)
|
|
301
|
+
if (is.null(raw) || !is.list(raw) || is.null(names(raw))) {
|
|
302
|
+
return(modifyList(empty, list(status = "invalid",
|
|
303
|
+
error = "The settings file could not be read as JSON.")))
|
|
304
|
+
}
|
|
305
|
+
if (!identical(as.integer(raw$version %||% 0L), SETTINGS_VERSION)) {
|
|
306
|
+
return(modifyList(empty, list(status = "invalid",
|
|
307
|
+
error = "The settings file is a version this CarmaR does not know.")))
|
|
308
|
+
}
|
|
309
|
+
block <- raw$settings
|
|
310
|
+
if (is.null(block)) block <- list()
|
|
311
|
+
if (!is.list(block) || (length(block) && is.null(names(block)))) {
|
|
312
|
+
return(modifyList(empty, list(status = "invalid",
|
|
313
|
+
error = "The settings file's `settings` is not an object.")))
|
|
314
|
+
}
|
|
315
|
+
values <- list()
|
|
316
|
+
ignored <- character()
|
|
317
|
+
rejected <- list()
|
|
318
|
+
for (key in names(block)) {
|
|
319
|
+
verdict <- carmar_settings_validate(key, block[[key]])
|
|
320
|
+
if (isTRUE(verdict$ok)) {
|
|
321
|
+
values[[key]] <- verdict$value
|
|
322
|
+
} else if (identical(verdict$reason, "unknown setting")) {
|
|
323
|
+
ignored <- c(ignored, key)
|
|
324
|
+
} else {
|
|
325
|
+
rejected[[length(rejected) + 1L]] <- list(key = key, reason = verdict$reason)
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
list(status = "ok", path = path, values = values,
|
|
329
|
+
ignored = ignored, rejected = rejected, error = "")
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
#' Write the whole settings block. Create → chmod 0600 → CHECK → write, the
|
|
333
|
+
#' ordering ai_key_write() uses and for the same reason: a file that was
|
|
334
|
+
#' briefly group-writable was briefly tamperable.
|
|
335
|
+
carmar_settings_write <- function(values, path = carmar_settings_path()) {
|
|
336
|
+
if (!file.exists(path) && !file.create(path, showWarnings = FALSE)) return(FALSE)
|
|
337
|
+
if (!isTRUE(Sys.chmod(path, "0600"))) {
|
|
338
|
+
unlink(path)
|
|
339
|
+
return(FALSE)
|
|
340
|
+
}
|
|
341
|
+
body <- list(version = SETTINGS_VERSION,
|
|
342
|
+
settings = if (length(values)) values else structure(list(), names = character()))
|
|
343
|
+
ok <- tryCatch({
|
|
344
|
+
writeLines(jsonlite::toJSON(body, auto_unbox = TRUE, pretty = TRUE, null = "null"), path)
|
|
345
|
+
TRUE
|
|
346
|
+
}, error = function(e) FALSE)
|
|
347
|
+
ok
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
`%||%` <- function(a, b) if (is.null(a)) b else a
|
|
351
|
+
|
|
352
|
+
#' The whole decision: what every setting resolves to, and from where.
|
|
353
|
+
#'
|
|
354
|
+
#' @return list(settings = list of per-key records, admin = list, file = the read)
|
|
355
|
+
carmar_settings_resolve <- function(env = Sys.getenv, file = NULL) {
|
|
356
|
+
if (is.null(file)) file <- carmar_settings_read()
|
|
357
|
+
usable <- identical(file$status, "ok")
|
|
358
|
+
|
|
359
|
+
out <- lapply(SETTINGS, function(s) {
|
|
360
|
+
raw <- env(s$env, "")
|
|
361
|
+
from_env <- nzchar(raw)
|
|
362
|
+
value <- s$default
|
|
363
|
+
source <- "default"
|
|
364
|
+
if (from_env) {
|
|
365
|
+
source <- "env"
|
|
366
|
+
value <- if (identical(s$kind, "bool")) {
|
|
367
|
+
# CARMAR_NO_HISTORY and CARMAR_NO_NATIVE_DIALOG are NEGATIVE flags —
|
|
368
|
+
# any value means "off" — while the file and the UI hold the positive.
|
|
369
|
+
# Storing the positive is what a checkbox actually is; translating
|
|
370
|
+
# happens here, once.
|
|
371
|
+
if (isTRUE(s$inverted)) FALSE else TRUE
|
|
372
|
+
} else if (identical(s$kind, "number")) {
|
|
373
|
+
n <- suppressWarnings(as.numeric(raw))
|
|
374
|
+
if (is.finite(n)) n else s$default
|
|
375
|
+
} else raw
|
|
376
|
+
} else if (usable && !is.null(file$values[[s$key]])) {
|
|
377
|
+
source <- "file"
|
|
378
|
+
value <- file$values[[s$key]]
|
|
379
|
+
}
|
|
380
|
+
list(key = s$key, label = s$label, kind = s$kind, effect = s$effect,
|
|
381
|
+
value = value, source = source, locked = from_env,
|
|
382
|
+
writable = !from_env, min = s$min, max = s$max, env = s$env)
|
|
383
|
+
})
|
|
384
|
+
names(out) <- vapply(SETTINGS, function(s) s$key, character(1))
|
|
385
|
+
|
|
386
|
+
admin <- lapply(SETTINGS_ADMIN, function(name) {
|
|
387
|
+
raw <- env(name, "")
|
|
388
|
+
list(key = name, value = raw, source = if (nzchar(raw)) "env" else "default")
|
|
389
|
+
})
|
|
390
|
+
|
|
391
|
+
list(settings = out, admin = admin, file = file)
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
#' The resolved scalar for one key.
|
|
395
|
+
carmar_settings_value <- function(resolved, key) {
|
|
396
|
+
entry <- resolved$settings[[key]]
|
|
397
|
+
if (is.null(entry)) NULL else entry$value
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
#' What a CHILD process should inherit — the settings that only take effect in
|
|
401
|
+
#' a freshly spawned worker, job or analyzer, expressed back in the environment
|
|
402
|
+
#' vocabulary those processes read.
|
|
403
|
+
carmar_settings_child_env <- function(resolved) {
|
|
404
|
+
out <- character()
|
|
405
|
+
q <- carmar_settings_value(resolved, "quarto_path")
|
|
406
|
+
if (is.character(q) && nzchar(q)) out["CARMAR_QUARTO"] <- q
|
|
407
|
+
b <- carmar_settings_value(resolved, "analyze_budget_s")
|
|
408
|
+
if (is.numeric(b)) out["CARMAR_ANALYZE_BUDGET"] <- as.character(b)
|
|
409
|
+
out
|
|
410
|
+
}
|