pantheon-opencode 1.5.2 → 1.6.0-beta.2
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/.pantheon/code-mode/README.md +4 -2
- package/.pantheon/code-mode/checkpoint_session.py +138 -102
- package/.pantheon/code-mode/compress-inline.py +102 -57
- package/.pantheon/code-mode/manifest.json +2 -8
- package/CHANGELOG.md +59 -0
- package/README.md +115 -55
- package/README.pt-BR.md +86 -43
- package/commands/pantheon-optimize.md +2 -2
- package/package.json +13 -1
- package/plugin.json +1 -1
- package/pyproject.toml +8 -15
- package/scripts/build-agents-md.mjs +2 -2
- package/scripts/check-plugin-v2-coverage.mjs +213 -0
- package/scripts/ci-validate-yaml.py +7 -5
- package/scripts/code_mode_server.py +116 -24
- package/scripts/doctor.mjs +85 -28
- package/scripts/install/cli-ui.mjs +1 -1
- package/scripts/install/config-migration.mjs +124 -3
- package/scripts/install/health-check.mjs +6 -19
- package/scripts/install/interactive.mjs +6 -6
- package/scripts/install/migrate.mjs +1 -1
- package/scripts/install/opencode.mjs +43 -10
- package/scripts/mcp_persistence_server.py +390 -76
- package/scripts/memory_mcp_server.py +370 -196
- package/scripts/postinstall.mjs +0 -7
- package/scripts/probe-context-rehydrate.mjs +28 -11
- package/scripts/prune.mjs +2 -2
- package/scripts/test-opencode-v2-sandbox.sh +1759 -0
- package/scripts/validate-routing.mjs +40 -0
- package/skills-lock.json +9 -10
- package/src/agents/aphrodite.md +0 -1
- package/src/agents/apollo.md +0 -1
- package/src/agents/athena.md +0 -1
- package/src/agents/demeter.md +0 -1
- package/src/agents/gaia.md +0 -1
- package/src/agents/hephaestus.md +0 -1
- package/src/agents/hermes.md +0 -1
- package/src/agents/iris.md +0 -1
- package/src/agents/mnemosyne.md +15 -17
- package/src/agents/nyx.md +0 -1
- package/src/agents/prometheus.md +0 -1
- package/src/agents/talos.md +0 -1
- package/src/agents/themis.md +0 -1
- package/src/agents/zeus.md +1 -3
- package/src/mcp/code_mode_server.py +116 -24
- package/src/mcp/mcp_codemap_module.py +559 -129
- package/src/mcp/mcp_persistence_server.py +390 -76
- package/src/mcp/memory_mcp_server.py +371 -248
- package/src/mcp/pantheon_vision_server.py +136 -24
- package/src/mcp/requirements-mcp.txt +5 -3
- package/src/mcp/requirements-vision.txt +2 -0
- package/src/pantheon/model-command.ts +18 -7
- package/src/pantheon/presets.d.mts +0 -8
- package/src/pantheon/presets.mjs +0 -29
- package/src/pantheon/todo-enforcer.ts +1 -1
- package/src/pantheon/vision.ts +1 -1
- package/src/plugin-v2.ts +134 -104
- package/src/plugins/tui/dist/tui.js +1 -1
- package/src/plugins/tui/package-lock.json +23 -23
- package/src/plugins/tui/package.json +12 -12
- package/src/routing.yml +0 -16
- package/src/skills/README.md +31 -36
- package/src/skills/auto-continue/SKILL.md +27 -77
- package/.pantheon/code-mode/eval-llm-judge.py +0 -255
- package/.pantheon/code-mode/eval-monte-carlo.py +0 -368
- package/.pantheon/code-mode/eval-run.py +0 -197
- package/.pantheon/code-mode/eval-static.py +0 -321
- package/.pantheon/code-mode/session-end-save.py +0 -186
- package/.pantheon/code-mode/session-end-save.sh +0 -18
- package/scripts/recover-zenodo-deposition.mjs +0 -146
- package/scripts/test-opencode-v1-v2-sandbox.sh +0 -862
- package/scripts/validate-zenodo-release.mjs +0 -154
- package/scripts/zenodo-artifact.mjs +0 -94
- package/scripts/zenodo-release-state.mjs +0 -53
- package/src/skills/orchestration-workflow/SKILL.md +0 -311
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
|
|
2
3
|
/**
|
|
3
4
|
* validate-routing.mjs — Validate routing.yml consistency
|
|
4
5
|
*
|
|
@@ -14,6 +15,7 @@
|
|
|
14
15
|
* node scripts/validate-routing.mjs --verbose # show all entries checked
|
|
15
16
|
*/
|
|
16
17
|
|
|
18
|
+
import { createHash } from 'node:crypto'
|
|
17
19
|
import { existsSync, readdirSync, readFileSync } from 'node:fs'
|
|
18
20
|
import { dirname, join } from 'node:path'
|
|
19
21
|
import { fileURLToPath } from 'node:url'
|
|
@@ -106,6 +108,44 @@ for (const [name, info] of Object.entries(routing.agents || {})) {
|
|
|
106
108
|
}
|
|
107
109
|
}
|
|
108
110
|
|
|
111
|
+
// B2. skills-lock.json must match src/skills/*/SKILL.md exactly — content
|
|
112
|
+
// digests stay in sync, and the set of locked paths equals the set on disk
|
|
113
|
+
// (so adding/removing a skill without updating the lock fails the gate).
|
|
114
|
+
console.log('\n skills-lock.json integrity:')
|
|
115
|
+
const lockPath = join(ROOT, 'skills-lock.json')
|
|
116
|
+
let skillLock = null
|
|
117
|
+
try {
|
|
118
|
+
skillLock = JSON.parse(readFileSync(lockPath, 'utf8'))
|
|
119
|
+
} catch (err) {
|
|
120
|
+
check(false, `skills-lock.json missing or malformed: ${err.message}`)
|
|
121
|
+
}
|
|
122
|
+
if (skillLock && typeof skillLock === 'object' && !Array.isArray(skillLock)) {
|
|
123
|
+
const lockedFiles = Object.keys(skillLock).sort()
|
|
124
|
+
const skillFiles = existsSync(srcSkillsDir)
|
|
125
|
+
? readdirSync(srcSkillsDir)
|
|
126
|
+
.filter((d) => existsSync(join(srcSkillsDir, d, 'SKILL.md')))
|
|
127
|
+
.map((d) => `src/skills/${d}/SKILL.md`)
|
|
128
|
+
.sort()
|
|
129
|
+
: []
|
|
130
|
+
console.log(` Locked skills: ${lockedFiles.length} (on disk: ${skillFiles.length})`)
|
|
131
|
+
|
|
132
|
+
for (const rel of skillFiles) {
|
|
133
|
+
const actual = createHash('sha256')
|
|
134
|
+
.update(readFileSync(join(ROOT, rel)))
|
|
135
|
+
.digest('hex')
|
|
136
|
+
check(
|
|
137
|
+
skillLock[rel] === actual,
|
|
138
|
+
`skills-lock.json stale for "${rel}": expected ${actual}, got ${skillLock[rel] ?? 'no entry'}`,
|
|
139
|
+
)
|
|
140
|
+
}
|
|
141
|
+
for (const rel of lockedFiles) {
|
|
142
|
+
check(
|
|
143
|
+
skillFiles.includes(rel),
|
|
144
|
+
`skills-lock.json lists "${rel}" but that SKILL.md does not exist`,
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
109
149
|
// C. Handoff validation
|
|
110
150
|
console.log(
|
|
111
151
|
`\n Handoff definitions: ${Object.keys(routing.handoffs || {}).length} agents with handoffs`,
|
package/skills-lock.json
CHANGED
|
@@ -1,23 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"src/skills/agent-coordination/SKILL.md": "cfd040ef29e3dd0fd5e0e67773874facf7c06e3284898afe1fddba4456c3f1aa",
|
|
3
3
|
"src/skills/artifact-management/SKILL.md": "c60e493666bedf46e9113d0dd20106489d5c5a73905e1370fcfc1c0d71c59185",
|
|
4
|
-
"src/skills/auto-continue/SKILL.md": "
|
|
4
|
+
"src/skills/auto-continue/SKILL.md": "4fe256d44ee9828d622a0d8b63cfc6bb39a317189a9ccf19494a6efd4527a7a9",
|
|
5
|
+
"src/skills/clonedeps/SKILL.md": "3e0b43cf96da98c83699dc48db3fd58fbfda84b9022bb70d3747875c81c3e124",
|
|
5
6
|
"src/skills/code-review-checklist/SKILL.md": "0e59367c65ef4608db80a0674279f32cfc3f67dca7236f233b61a47fe506579f",
|
|
6
|
-
"src/skills/
|
|
7
|
+
"src/skills/codemap/SKILL.md": "87af5271e1576e154cadff313eab16264b726e43d094520736b7a5b03b409a4a",
|
|
8
|
+
"src/skills/context-compression/SKILL.md": "ebc721c715c1fa20e4ebf5177007fde94e001540b16cef4316393a665633c16f",
|
|
7
9
|
"src/skills/git-workflow-and-versioning/SKILL.md": "685a94041f1b4ce2a430301f517b89102055696267c369a7e8fb191606659276",
|
|
8
10
|
"src/skills/incremental-implementation/SKILL.md": "7065ddb45da3c6b6751de07f290e92e6cb072e949a2e3aaae13ee7762f0a70fc",
|
|
11
|
+
"src/skills/loop-engineering/SKILL.md": "f804e545821b9cca8fe0ec517307ed902446f28c2ca8ff9194c0f987cf7229e3",
|
|
9
12
|
"src/skills/memory-bank/SKILL.md": "4717cf18b43550c9f02cfab810968b97a5b2584f00a15516ecba643c80cdc110",
|
|
10
|
-
"src/skills/
|
|
13
|
+
"src/skills/reflect/SKILL.md": "4c7608c2a55cb8a50ac90fc0e2446bdf25e7914b1748cb7731c311d13c29dfdd",
|
|
11
14
|
"src/skills/security-hardening/SKILL.md": "6d637e122421f7bc3b751f04854ad343a5a5d35249458bc8939233138fd3f20d",
|
|
12
15
|
"src/skills/session-goal/SKILL.md": "793498265f70253c20524f6adc88bcc86db11d7160fe8232a26aedc6b1c5489f",
|
|
16
|
+
"src/skills/simplify/SKILL.md": "aa739e9f75962a77d6574557d674ea01b5aea95e580b721d66c058753161b053",
|
|
13
17
|
"src/skills/spec-driven-development/SKILL.md": "e93b9103d6cd1848ab01328efde82c1daf564d1cabbdc2278254b346fcf55d02",
|
|
14
18
|
"src/skills/tdd-with-agents/SKILL.md": "9880919a46b74f549f948147d8b086b008635de94fa259f76261d067772bb8e3",
|
|
19
|
+
"src/skills/verification-planning/SKILL.md": "14186446ef51e1ba2c87b337e8d5f86e566d805156ab21fba2a75f8c4a99df3f",
|
|
15
20
|
"src/skills/visual-review-pipeline/SKILL.md": "f6e65ff75a9325c38e0e9dde8e87f09b873e14e490936ef48aa80201bb68fe10",
|
|
16
|
-
"src/skills/
|
|
17
|
-
"src/skills/simplify/SKILL.md": "src/skills/simplify/SKILL.md",
|
|
18
|
-
"src/skills/reflect/SKILL.md": "src/skills/reflect/SKILL.md",
|
|
19
|
-
"src/skills/worktrees/SKILL.md": "src/skills/worktrees/SKILL.md",
|
|
20
|
-
"src/skills/verification-planning/SKILL.md": "src/skills/verification-planning/SKILL.md",
|
|
21
|
-
"src/skills/loop-engineering/SKILL.md": "src/skills/loop-engineering/SKILL.md",
|
|
22
|
-
"src/skills/clonedeps/SKILL.md": "src/skills/clonedeps/SKILL.md"
|
|
21
|
+
"src/skills/worktrees/SKILL.md": "0033c5f8a50f210f31f0256ba8cc0fd99c65674a2aabedb05d8e69447b5ec218"
|
|
23
22
|
}
|
package/src/agents/aphrodite.md
CHANGED
package/src/agents/apollo.md
CHANGED
package/src/agents/athena.md
CHANGED
package/src/agents/demeter.md
CHANGED
package/src/agents/gaia.md
CHANGED
package/src/agents/hephaestus.md
CHANGED
package/src/agents/hermes.md
CHANGED
package/src/agents/iris.md
CHANGED
package/src/agents/mnemosyne.md
CHANGED
|
@@ -6,7 +6,6 @@ description: Memory bank quality owner — initializes .pantheon/memory-bank/, w
|
|
|
6
6
|
mode: all
|
|
7
7
|
reasoning_effort: low
|
|
8
8
|
|
|
9
|
-
steps: 25
|
|
10
9
|
mcp_tools:
|
|
11
10
|
pantheon-resources: all
|
|
12
11
|
pantheon-memory:
|
|
@@ -114,7 +113,7 @@ Mnemosyne executes the expanded compression pipeline. When Zeus delegates compre
|
|
|
114
113
|
- Append new entries to `_xref/index.md`
|
|
115
114
|
- Increment `_xref/_next_id.json`
|
|
116
115
|
|
|
117
|
-
7. **Auto-index
|
|
116
|
+
7. **Auto-index memory**: Store the new entries with the `memory_store` MCP tool (namespace `memory-bank`) so `memory_search` can find them. Search is FTS5 (BM25) keyword retrieval — there is no embedding step to run or report on.
|
|
118
117
|
|
|
119
118
|
8. **Report**: Return summary: "Compressed. 2 CRITICAL, 1 HIGH, 3 STANDARD. Budget: 15/20 lines. Cross-refs: +2 entities, +1 decision. Indexed X new, skipped Y duplicates."
|
|
120
119
|
|
|
@@ -127,16 +126,16 @@ Mnemosyne executes the expanded compression pipeline. When Zeus delegates compre
|
|
|
127
126
|
- NEVER write over existing entries (idempotency by date+phase+agent hash)
|
|
128
127
|
- NEVER delete _xref/ entries (append-only)
|
|
129
128
|
|
|
130
|
-
##
|
|
129
|
+
## Recall Handler (Level 3)
|
|
131
130
|
|
|
132
|
-
Mnemosyne
|
|
131
|
+
Mnemosyne recalls stored entries with the `memory_search` MCP tool:
|
|
133
132
|
|
|
134
133
|
**Command:** `@mnemosyne Recall "<query>" [--top-k 5] [--type adr|subtask|wisdom|impl|decision] [--agent hermes] [--since 2026-01-01] [--tags auth,jwt]`
|
|
135
134
|
|
|
136
135
|
**How it works:**
|
|
137
|
-
1. Calls `
|
|
138
|
-
2. Returns ranked, structured results with scores and source
|
|
139
|
-
3.
|
|
136
|
+
1. Calls `memory_search` with the query, namespace, and `top_k`
|
|
137
|
+
2. Returns ranked, structured results with BM25 scores and source keys
|
|
138
|
+
3. Retrieval is lexical: stopwords are dropped and terms of 4+ characters are prefix-matched, so queries should use words that actually appear in the stored text. Filter by type/agent/tags in the caller — `memory_search` filters on `namespace` only.
|
|
140
139
|
|
|
141
140
|
**Usage examples:**
|
|
142
141
|
```
|
|
@@ -146,14 +145,13 @@ Mnemosyne provides semantic recall via the Level 3 Vector Memory system:
|
|
|
146
145
|
```
|
|
147
146
|
|
|
148
147
|
**Integration with compress_context:**
|
|
149
|
-
After each `compress_context` run,
|
|
150
|
-
1.
|
|
148
|
+
After each `compress_context` run, store the new entries with `memory_store`:
|
|
149
|
+
1. Store each new entry with namespace `memory-bank` and a `memory-bank/<id>` key
|
|
151
150
|
2. Report: "Indexed X new memories, skipped Y duplicates"
|
|
152
|
-
3. If sentence-transformers is not installed, skip vector indexing but still index FTS5
|
|
153
151
|
|
|
154
152
|
**Integration with Close sprint:**
|
|
155
153
|
When `Close sprint` is called, before wiping .tmp/:
|
|
156
|
-
1.
|
|
154
|
+
1. Store the final batch of entries
|
|
157
155
|
2. Report final index stats
|
|
158
156
|
|
|
159
157
|
## Invocation Rules
|
|
@@ -164,8 +162,8 @@ When `Close sprint` is called, before wiping .tmp/:
|
|
|
164
162
|
## Quick-Index Handler (Tier 1 — Background Agent Results)
|
|
165
163
|
|
|
166
164
|
Called automatically by Zeus when any agent returns a subtask_summary
|
|
167
|
-
(background or foreground). Persists results
|
|
168
|
-
no Themis needed.
|
|
165
|
+
(background or foreground). Persists results via the `memory_store` MCP tool
|
|
166
|
+
immediately, no Themis needed.
|
|
169
167
|
|
|
170
168
|
**Trigger patterns:**
|
|
171
169
|
- Background agent completes → Zeus calls Mnemosyne Quick-index
|
|
@@ -175,9 +173,9 @@ no Themis needed.
|
|
|
175
173
|
**Command:** `@mnemosyne Quick-index <subtask_summary_json>`
|
|
176
174
|
|
|
177
175
|
**What it does:**
|
|
178
|
-
1. Calls `
|
|
179
|
-
2.
|
|
180
|
-
3. Reports: "Indexed: {type} from @{agent} ({
|
|
176
|
+
1. Calls `memory_store` with the summary fields
|
|
177
|
+
2. Stores under a `type/agent` namespace and a `<type>:<agent>:<date>` key
|
|
178
|
+
3. Reports: "Indexed: {type} from @{agent} (namespace {namespace})"
|
|
181
179
|
|
|
182
180
|
**Parameters (from subtask_summary):**
|
|
183
181
|
| Field | Source | Required |
|
|
@@ -194,7 +192,7 @@ no Themis needed.
|
|
|
194
192
|
**Safety:**
|
|
195
193
|
- [OK] Safe to call on partial results (indexes what's available)
|
|
196
194
|
- [OK] Safe to call multiple times (idempotent)
|
|
197
|
-
- [OK] Works
|
|
195
|
+
- [OK] Works with no embedding backend (FTS5 keyword search only)
|
|
198
196
|
- [FAIL] Does NOT generate ZZ artifact (that's Tier 2)
|
|
199
197
|
- [FAIL] Does NOT update 01-active-context.md (that's Tier 2)
|
|
200
198
|
|
package/src/agents/nyx.md
CHANGED
package/src/agents/prometheus.md
CHANGED
package/src/agents/talos.md
CHANGED
package/src/agents/themis.md
CHANGED
package/src/agents/zeus.md
CHANGED
|
@@ -9,7 +9,6 @@ permission:
|
|
|
9
9
|
task:
|
|
10
10
|
"*": allow
|
|
11
11
|
temperature: 0.2
|
|
12
|
-
steps: 45
|
|
13
12
|
mcp_tools:
|
|
14
13
|
pantheon-resources: all
|
|
15
14
|
pantheon-memory:
|
|
@@ -26,7 +25,6 @@ skills:
|
|
|
26
25
|
- artifact-management
|
|
27
26
|
- context-compression
|
|
28
27
|
- auto-continue
|
|
29
|
-
- orchestration-workflow
|
|
30
28
|
- incremental-implementation
|
|
31
29
|
|
|
32
30
|
---
|
|
@@ -202,7 +200,7 @@ Zeus (nivel 0) -> Apollo/Hermes (nivel 1) -> sub-subagente (nivel 2 max).
|
|
|
202
200
|
|
|
203
201
|
| Tier | Trigger | Action |
|
|
204
202
|
|------|---------|--------|
|
|
205
|
-
| Tier 1 — Auto-index | Any agent returns subtask_summary | `memory_store()` direto ->
|
|
203
|
+
| Tier 1 — Auto-index | Any agent returns subtask_summary | `memory_store()` direto -> FTS5 memory |
|
|
206
204
|
| Tier 2 — Compression | Themis APPROVED | compress_context -> ZZ -> memory-bank |
|
|
207
205
|
|
|
208
206
|
## MCP Tools
|
|
@@ -20,6 +20,11 @@ import hashlib
|
|
|
20
20
|
import json
|
|
21
21
|
import logging
|
|
22
22
|
import os
|
|
23
|
+
|
|
24
|
+
try:
|
|
25
|
+
import resource
|
|
26
|
+
except (ImportError, OSError, ValueError): # pragma: no cover - platform-specific
|
|
27
|
+
resource = None # type: ignore[assignment]
|
|
23
28
|
import shutil
|
|
24
29
|
import signal
|
|
25
30
|
import stat
|
|
@@ -125,6 +130,88 @@ def _build_script_env(
|
|
|
125
130
|
return env
|
|
126
131
|
|
|
127
132
|
|
|
133
|
+
# Additional tasks a sandboxed script may create beyond the UID's current
|
|
134
|
+
# count. RLIMIT_NPROC is a per-real-UID limit on the *total* number of
|
|
135
|
+
# processes AND threads, so the sandbox floor must clear everything the host
|
|
136
|
+
# already owns before a script can fork at all. This headroom is the actual
|
|
137
|
+
# anti-fork-bomb budget: the script still cannot exceed current + headroom.
|
|
138
|
+
NPROC_HEADROOM: int = 64
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _uid_task_count() -> int | None:
|
|
142
|
+
"""Count the tasks (processes + threads) this real UID already owns.
|
|
143
|
+
|
|
144
|
+
The kernel compares the post-fork total against RLIMIT_NPROC, so this is
|
|
145
|
+
the number the sandbox limit has to exceed. Returns *None* when the count
|
|
146
|
+
cannot be established (no readable ``/proc``), which makes the caller
|
|
147
|
+
fail open on ``--nproc`` rather than pin a value that may be too low and
|
|
148
|
+
break every script.
|
|
149
|
+
|
|
150
|
+
The tally is one too high per process: ``/proc/<pid>/task`` already lists
|
|
151
|
+
the thread-group leader, so the extra ``1`` counts it twice. That bias
|
|
152
|
+
only widens the derived limit (the fail-open direction), so it is left in
|
|
153
|
+
place deliberately.
|
|
154
|
+
"""
|
|
155
|
+
uid = os.getuid()
|
|
156
|
+
count = 0
|
|
157
|
+
try:
|
|
158
|
+
pids = os.listdir("/proc")
|
|
159
|
+
except OSError:
|
|
160
|
+
return None
|
|
161
|
+
for pid in pids:
|
|
162
|
+
if not pid.isdigit():
|
|
163
|
+
continue
|
|
164
|
+
proc_dir = f"/proc/{pid}"
|
|
165
|
+
try:
|
|
166
|
+
if os.stat(proc_dir).st_uid != uid:
|
|
167
|
+
continue
|
|
168
|
+
# ``/proc/<pid>/task`` already includes the thread-group leader,
|
|
169
|
+
# so the extra 1 counts it twice (over-count by one per process).
|
|
170
|
+
count += 1 + len(os.listdir(f"{proc_dir}/task"))
|
|
171
|
+
except OSError:
|
|
172
|
+
# Process exited between listdir and stat, or is not ours to read.
|
|
173
|
+
continue
|
|
174
|
+
return count
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _sandbox_nproc() -> str | None:
|
|
178
|
+
"""Return the ``--nproc`` value for a sandboxed script, or *None*.
|
|
179
|
+
|
|
180
|
+
Derived from the host rather than hardcoded: the value is the UID's
|
|
181
|
+
current task count plus ``NPROC_HEADROOM``, clamped to the hard limit
|
|
182
|
+
(``prlimit`` fails with EPERM above it, and an unprivileged process cannot
|
|
183
|
+
raise its soft limit past the hard one).
|
|
184
|
+
|
|
185
|
+
Returns *None* when the count or the limit cannot be read, in which case
|
|
186
|
+
the caller omits ``--nproc``. ``--as`` and ``--cpu`` still apply, so the
|
|
187
|
+
sandbox keeps its memory and CPU boundaries.
|
|
188
|
+
"""
|
|
189
|
+
current = _uid_task_count()
|
|
190
|
+
if current is None:
|
|
191
|
+
_log.warning(
|
|
192
|
+
"code-mode: cannot count this UID's tasks; omitting --nproc, so "
|
|
193
|
+
"the sandboxed script's fork budget is unbounded"
|
|
194
|
+
)
|
|
195
|
+
return None
|
|
196
|
+
if resource is None:
|
|
197
|
+
_log.warning(
|
|
198
|
+
"code-mode: cannot read RLIMIT_NPROC; omitting --nproc, so the "
|
|
199
|
+
"sandboxed script's fork budget is unbounded"
|
|
200
|
+
)
|
|
201
|
+
return None
|
|
202
|
+
try:
|
|
203
|
+
_soft, hard = resource.getrlimit(resource.RLIMIT_NPROC)
|
|
204
|
+
except (OSError, ValueError):
|
|
205
|
+
_log.warning(
|
|
206
|
+
"code-mode: cannot read RLIMIT_NPROC; omitting --nproc, so the "
|
|
207
|
+
"sandboxed script's fork budget is unbounded"
|
|
208
|
+
)
|
|
209
|
+
return None
|
|
210
|
+
if hard == getattr(resource, "RLIM_INFINITY", -1):
|
|
211
|
+
return str(current + NPROC_HEADROOM)
|
|
212
|
+
return str(max(1, min(current + NPROC_HEADROOM, hard)))
|
|
213
|
+
|
|
214
|
+
|
|
128
215
|
def _prlimit_prefix(
|
|
129
216
|
prlimit_path: str | None,
|
|
130
217
|
timeout_s: int = SCRIPT_TIMEOUT,
|
|
@@ -132,21 +219,34 @@ def _prlimit_prefix(
|
|
|
132
219
|
"""Return a ``prlimit`` command prefix to wrap a subprocess, or *None*.
|
|
133
220
|
|
|
134
221
|
On Linux with ``prlimit`` available the prefix adds:
|
|
135
|
-
--nproc
|
|
136
|
-
|
|
137
|
-
|
|
222
|
+
--nproc=<uid task count + NPROC_HEADROOM> --as=1073741824
|
|
223
|
+
--cpu=<timeout_s + 5>
|
|
224
|
+
|
|
225
|
+
``--nproc`` is derived per invocation by :func:`_sandbox_nproc` rather
|
|
226
|
+
than hardcoded. RLIMIT_NPROC is a per-real-UID total across processes AND
|
|
227
|
+
threads, so a fixed value below the host's current usage makes the very
|
|
228
|
+
first ``fork()`` inside the sandbox fail with EAGAIN — which is exactly
|
|
229
|
+
what a hardcoded ``--nproc=512`` did on a host already running more than
|
|
230
|
+
512 tasks. The isolation property is unchanged: a script still cannot
|
|
231
|
+
create more than ``NPROC_HEADROOM`` additional tasks.
|
|
232
|
+
|
|
233
|
+
On non-Linux, when *prlimit_path* is falsy, or when the nproc limit cannot
|
|
234
|
+
be derived, the flag is omitted (fail-open); ``--as``/``--cpu`` remain.
|
|
138
235
|
"""
|
|
139
236
|
if not prlimit_path:
|
|
140
237
|
return None
|
|
141
238
|
|
|
142
239
|
cpu_limit = timeout_s + 5
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
240
|
+
prefix = [prlimit_path]
|
|
241
|
+
nproc = _sandbox_nproc()
|
|
242
|
+
if nproc is not None:
|
|
243
|
+
prefix.append(f"--nproc={nproc}")
|
|
244
|
+
prefix += [
|
|
146
245
|
"--as=1073741824", # 1 GiB in bytes
|
|
147
246
|
f"--cpu={cpu_limit}",
|
|
148
247
|
"--",
|
|
149
248
|
]
|
|
249
|
+
return prefix
|
|
150
250
|
|
|
151
251
|
|
|
152
252
|
def _detect_prlimit() -> str | None:
|
|
@@ -226,7 +326,9 @@ def _packaged_scripts_dir() -> Path | None:
|
|
|
226
326
|
return None
|
|
227
327
|
|
|
228
328
|
|
|
229
|
-
def _resolve_scripts_dir(
|
|
329
|
+
def _resolve_scripts_dir(
|
|
330
|
+
candidates: list[Path], *, allow_packaged: bool = True
|
|
331
|
+
) -> Path | None:
|
|
230
332
|
"""Select the first usable candidate, optionally trying the package copy."""
|
|
231
333
|
for candidate in candidates:
|
|
232
334
|
if _has_usable_scripts(candidate):
|
|
@@ -377,9 +479,7 @@ def _load_manifest(scripts_dir: Path | None = None) -> dict[str, str]:
|
|
|
377
479
|
) from None
|
|
378
480
|
|
|
379
481
|
if not isinstance(data, dict):
|
|
380
|
-
raise ManifestError(
|
|
381
|
-
"CORRUPT_DATA", "Code-mode manifest must be a JSON object."
|
|
382
|
-
)
|
|
482
|
+
raise ManifestError("CORRUPT_DATA", "Code-mode manifest must be a JSON object.")
|
|
383
483
|
|
|
384
484
|
if data.get("version") != 1:
|
|
385
485
|
raise ManifestError(
|
|
@@ -527,7 +627,6 @@ def _contract_result(
|
|
|
527
627
|
return f"[{status}] {message}"
|
|
528
628
|
|
|
529
629
|
|
|
530
|
-
|
|
531
630
|
def _format_output(
|
|
532
631
|
stdout: str,
|
|
533
632
|
stderr: str,
|
|
@@ -818,22 +917,15 @@ async def execute_code_script(
|
|
|
818
917
|
timeout_s=timeout_s,
|
|
819
918
|
status="TIMEOUT",
|
|
820
919
|
)
|
|
821
|
-
except FileNotFoundError:
|
|
822
|
-
return _build_result(
|
|
823
|
-
"",
|
|
824
|
-
f"Script '{script_name}' not found or interpreter missing.",
|
|
825
|
-
-1,
|
|
826
|
-
timed_out=False,
|
|
827
|
-
duration_ms=0,
|
|
828
|
-
metadata=metadata,
|
|
829
|
-
json_output=json_output,
|
|
830
|
-
timeout_s=timeout_s,
|
|
831
|
-
status="UNAVAILABLE",
|
|
832
|
-
)
|
|
833
920
|
except OSError as e:
|
|
921
|
+
error = (
|
|
922
|
+
f"Script '{script_name}' not found or interpreter missing."
|
|
923
|
+
if isinstance(e, FileNotFoundError)
|
|
924
|
+
else f"Failed to execute script: {e}"
|
|
925
|
+
)
|
|
834
926
|
return _build_result(
|
|
835
927
|
"",
|
|
836
|
-
|
|
928
|
+
error,
|
|
837
929
|
-1,
|
|
838
930
|
timed_out=False,
|
|
839
931
|
duration_ms=0,
|