') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); emrg: sandbox default rule — configured wins, else workspace-write by argszero · Pull Request #895 · argszero/emrg · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Agent.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -118,7 +118,7 @@ Community needs voiced in HN agent-UI discussions map directly to EMRG's design:
pkill -f "emrg.server"; rm -f ~/.emrg/emrgd.token; python -m emrg
```

Python: `uv run pytest tests/ -v` (982) — import check: `uv run python -c "from emrg.client.app import run_client"`
Python: `uv run pytest tests/ -v` (983) — import check: `uv run python -c "from emrg.client.app import run_client"`
GUI: `cd emrg/gui && npm test` (260: 45 daemon_client + 19 conn-manager + 22 app-commands + 131 renderer smoke + 16 i18n + 7 integration + 3 commands + 8 build-config + 7 gui-state + 2 tool-group) — syntax: `node --check main.js preload.js daemon_client.js renderer/js/*.js`
CI: `uv run pytest` (ubuntu + **windows-2025 matrix** — Windows pytest 回归在 PR CI 即失败,v0.2.29 教训 #725) + GUI tests + **actionlint workflow lint** (`rhysd/actionlint@v1.7.12` gate, #444 — workflow 解析错误在 PR CI 即失败,如 `if:` secrets 上下文)
Re-trigger: `scripts/re-trigger-ci.sh [branch]` (workflow_dispatch, #527 — 替代空 commit 重触发:Actions outage 会整段丢弃 push 事件,dispatch 走 API 路径不受影响)
Expand Down
28 changes: 13 additions & 15 deletions emrg/server/scheduler.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -284,32 +284,30 @@ def __init__(
self._repo_configured = project_name == "emrg"
self._session_id = f"emrg-evolution-{name}"
self._source_dir = path or name
# Sandbox tier for this task's bash tool (rant 2026-08-20T15:46:50):
# explicit config wins; builtin tasks get suggested defaults; None =
# danger-full-access (current behavior).
self._sandbox = self._resolve_sandbox(name, config, sandbox)
# Sandbox tier for this task's bash tool (rant 2026-08-20T15:46:50 +
# 18:05:20): explicit config wins, otherwise unified default
# workspace-write. No task-name builtin defaults, no implicit
# danger-full-access fallback.
self._sandbox = self._resolve_sandbox(config, sandbox)
if self._sandbox:
self._logger.info(
"TaskHandler[%s]: bash sandbox tier = %s", name, self._sandbox
)

@staticmethod
def _resolve_sandbox(name: str, config: dict, explicit: str | None) -> str | None:
def _resolve_sandbox(config: dict, explicit: str | None) -> str:
"""Effective bash sandbox tier for a task.

Order: tasks.yml top-level ``sandbox:`` field → ``config.sandbox`` →
builtin defaults by task name → None (= danger-full-access, the
existing un-sandboxed behavior). Invalid values fall through to the
defaults rather than breaking the task.
Rant 2026-08-20T18:05:20: unified rule — configured value wins,
otherwise ``"workspace-write"``. No task-name builtin defaults
(emrg-task/opensource special cases removed), no implicit
danger-full-access fallback. Invalid values fall through to the
default rather than breaking the task.
"""
for cand in (explicit, config.get("sandbox")):
if cand in SANDBOX_MODES and cand != "danger-full-access":
if cand in SANDBOX_MODES:
return cand
if name == "emrg-task":
return "workspace-write" # evolution writes its own repo
if name.endswith("-opensource-task"):
return "read-only" # community work in host-owned repos
return None
return "workspace-write"

# ── Saturation state (restored from disk across daemon restarts) ──

Expand Down
34 changes: 34 additions & 0 deletions tests/test_scheduler.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -2075,3 +2075,37 @@ def test_evolution_template_renders_dual_project_match():
assert "emrg" in out, "task.project 值应渲染"
assert "argszero/emrg" in out, "owner/repo 形式应渲染"
assert "ignore rants without a `project` field entirely" in out


# ── sandbox tier resolution (rant 2026-08-20T18:05:20) ─────────────


def test_sandbox_resolution_unified_default_rule():
"""Rant 2026-08-20T18:05:20: no name-based builtin defaults — the
configured value wins, otherwise the unified default is workspace-write.
There is no implicit danger-full-access fallback."""
# explicit tasks.yml top-level sandbox wins
assert TaskHandler._resolve_sandbox({}, "read-only") == "read-only"
# config.sandbox used when no explicit value
assert TaskHandler._resolve_sandbox({"sandbox": "read-only"}, None) == "read-only"
# explicit beats config
assert (
TaskHandler._resolve_sandbox({"sandbox": "workspace-write"}, "danger-full-access")
== "danger-full-access"
)
# no name-based defaults: emrg-task no longer implies workspace-write
# via the old special case — it is the unified default now
handler = TaskHandler(
name="emrg-task", config={"project": "emrg"}, interval=60,
identity=InstanceIdentity(),
)
assert handler._sandbox == "workspace-write"
# plain task name also gets the unified default, not danger-full-access
handler2 = TaskHandler(
name="generic-task", config={}, interval=60,
identity=InstanceIdentity(),
)
assert handler2._sandbox == "workspace-write"
# invalid values fall through to the default
assert TaskHandler._resolve_sandbox({"sandbox": "bogus"}, None) == "workspace-write"
assert TaskHandler._resolve_sandbox({}, "bogus") == "workspace-write"
Loading