@stravica-ai/digital-operator-pa 0.1.37 → 0.1.39
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.
|
@@ -108,7 +108,7 @@ The failure mode is silent: launchd records exit code 78 (`EX_CONFIG`), the Stan
|
|
|
108
108
|
|
|
109
109
|
Proven live (w-2026-07-15-003, verified with `env -i`): cron's crond process provides a small fixed environment (`SHELL`, a narrow system `PATH` — `/usr/bin:/bin` — `HOME`, `LOGNAME`/`USER`), NOT a login shell's env. A schedule whose `command` relies on `pnpm`/`node` being on PATH via nvm silently fails to spawn once actually installed, even if it "worked" when tested from an interactive terminal.
|
|
110
110
|
|
|
111
|
-
The generated cron line handles this for you automatically: `scheduler install` detects `$HOME/.nvm/nvm.sh` on the target AT INSTALL TIME and, if present,
|
|
111
|
+
The generated cron line handles this for you automatically: `scheduler install` detects `$HOME/.nvm/nvm.sh` on the target AT INSTALL TIME and, if present, sources it inside the rendered chain. The generated cron line is `bash -c "{ date -u; . $HOME/.nvm/nvm.sh && cd <workDir> && <command>; } >> /tmp/<agent>-<id>.log 2>&1"` — the `{ ...; }` braces are load-bearing: without them the `>> log 2>&1` binds only to `<command>` and any earlier link failing (nvm-source, cd) sends its stderr to cron mail, which hosts with no MTA silently discard (w-2026-09-13-dave-042; 0.1.39 fix, smarthome01 crons fired but wrote no log 2026-09-09 -> 13). The leading `date -u` prints an ISO timestamp so a run that dies mid-chain still leaves dated evidence. If no nvm install is found, the preamble is simply omitted — nothing to do for schedules that only need system binaries. `environment.*` values are emitted as `export VAR='val';` statements ahead of the command (not a bare `VAR='val' cd ...` prefix, which in POSIX shell only scopes to the `cd` builtin and never actually reaches the command after `&&` — a real, silent bug the old generator had).
|
|
112
112
|
|
|
113
113
|
`scheduler verify` now runs this EXACT generated line (not a bare `sched.command`), under cron's actual minimal environment — never the verifying shell's `process.env`. This is deliberate: the old verify path inherited the operator's full login-shell env, which reliably masked this whole bug class. If `verify` reports `ok` today, that's an honest signal the schedule will actually fire under real cron.
|
|
114
114
|
|
|
@@ -354,7 +354,20 @@ export function buildCronCommand(sched, workDir, env, opts = {}) {
|
|
|
354
354
|
const exportStmts = Object.entries(env)
|
|
355
355
|
.map(([k, v]) => `export ${k}='${String(v).replace(/'/g, `'\\''`)}'; `)
|
|
356
356
|
.join('');
|
|
357
|
-
|
|
357
|
+
// Whole rendered chain is braced so `>> logPath 2>&1` binds to the group,
|
|
358
|
+
// not just the last simple command. Without the braces bash parses
|
|
359
|
+
// `A && B && cmd >> log 2>&1` with the redirect bound to `cmd` alone, so
|
|
360
|
+
// an nvm-source or `cd` failure earlier in the chain sends its stderr to
|
|
361
|
+
// cron mail (silently discarded on hosts with no MTA) and leaves the log
|
|
362
|
+
// file empty — the exact silent-failure class w-2026-09-13-dave-030 tracks
|
|
363
|
+
// (smarthome01 crons fired but never wrote a log line, 2026-09-09 -> 13).
|
|
364
|
+
// A leading `date -u` inside the group prints an ISO timestamp so a run
|
|
365
|
+
// that dies mid-chain still leaves dated evidence. No `$()` — cron passes
|
|
366
|
+
// the crontab line through /bin/sh -c '...' which parses the double-quoted
|
|
367
|
+
// bash-c argument and would eagerly expand $(...) at dispatch time, not
|
|
368
|
+
// at run time.
|
|
369
|
+
const chain = `date -u; ${preamble}${exportStmts}cd ${workDir} && ${sched.command}`;
|
|
370
|
+
const inner = `{ ${chain}; } >> ${logPath} 2>&1`;
|
|
358
371
|
const wrapped = `bash -c "${inner.replace(/"/g, '\\"')}"`;
|
|
359
372
|
return { inner, wrapped };
|
|
360
373
|
}
|
|
@@ -393,14 +393,24 @@ const server = createServer(async (req, res) => {
|
|
|
393
393
|
// outer pass has already validated the Host header, so this call is
|
|
394
394
|
// effectively the state-changing-method sweep.
|
|
395
395
|
if (pathname.startsWith('/__api/') || pathname.startsWith('/api/')) {
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
396
|
+
// Extension routes may opt into cross-origin acceptance via
|
|
397
|
+
// addRoute(..., { crossOrigin: true }). Look the route up before the
|
|
398
|
+
// state-changing sweep so an opted-in route can accept a POST from a
|
|
399
|
+
// foreign origin (e.g. the Dave Review bookmarklet firing on any web
|
|
400
|
+
// page). The outer per-request pass has already validated the Host
|
|
401
|
+
// header, so a crossOrigin route is still protected against DNS
|
|
402
|
+
// rebinding; it takes over Origin/CSRF policy through its own auth
|
|
403
|
+
// (a per-install token, in the case of the review submit route).
|
|
403
404
|
const extRoute = extRoutes.find(r => r.method === req.method && r.pathname === pathname);
|
|
405
|
+
if (!extRoute || extRoute.crossOrigin !== true) {
|
|
406
|
+
const guard = checkRequestGuards(req, ALLOWED_HOSTS, ALLOWED_ORIGINS);
|
|
407
|
+
if (!guard.ok) {
|
|
408
|
+
warn('[workspace] guard refused ' + req.method + ' ' + pathname + ' (' + guard.reason + ')');
|
|
409
|
+
res.writeHead(guard.status, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
410
|
+
res.end('Forbidden');
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
404
414
|
if (extRoute) { extRoute.handler(req, res, url, REPO_ROOT); return; }
|
|
405
415
|
|
|
406
416
|
// GET /__api/nav -- merged core + extension nav manifest, team-scoped to
|
|
@@ -577,13 +587,22 @@ async function loadExtensions() {
|
|
|
577
587
|
try {
|
|
578
588
|
const extApi = await import(extApiPath);
|
|
579
589
|
if (typeof extApi.register === 'function') {
|
|
580
|
-
extApi.register(function addRoute(method, pathname, handler) {
|
|
590
|
+
extApi.register(function addRoute(method, pathname, handler, options) {
|
|
591
|
+
// options.crossOrigin === true opts this route out of the API-branch
|
|
592
|
+
// Origin/Sec-Fetch-Site sweep. The outer Host allowlist still runs
|
|
593
|
+
// on every request, so a crossOrigin route accepts requests from a
|
|
594
|
+
// foreign browser Origin as long as the Host addresses this
|
|
595
|
+
// workspace via one of its own allowlisted names. The extension
|
|
596
|
+
// owns auth (a per-install token, say) and its own CORS response.
|
|
597
|
+
// Default is unchanged: undefined/false keeps the full guard.
|
|
598
|
+
const opts = (options && typeof options === 'object') ? options : {};
|
|
599
|
+
const crossOrigin = opts.crossOrigin === true;
|
|
581
600
|
const shadows = extRoutes.some(r => r.method === method && r.pathname === pathname);
|
|
582
601
|
if (!shadows) {
|
|
583
602
|
// Check if it shadows a core route (informational only).
|
|
584
|
-
log('[workspace] Extension route registered: ' + method + ' ' + pathname);
|
|
603
|
+
log('[workspace] Extension route registered: ' + method + ' ' + pathname + (crossOrigin ? ' [crossOrigin]' : ''));
|
|
585
604
|
}
|
|
586
|
-
extRoutes.push({ method: method, pathname: pathname, handler: handler });
|
|
605
|
+
extRoutes.push({ method: method, pathname: pathname, handler: handler, crossOrigin: crossOrigin });
|
|
587
606
|
});
|
|
588
607
|
}
|
|
589
608
|
} catch (e) {
|
package/package.json
CHANGED