pantheon-opencode 1.5.2 → 1.6.0-beta.10
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/AGENTS.global.md +182 -0
- package/AGENTS.md +10 -891
- package/CHANGELOG.md +343 -0
- package/README.md +577 -79
- package/README.pt-BR.md +345 -60
- package/bin/pantheon-init.mjs +26 -3
- package/commands/pantheon-audit.md +7 -1
- package/commands/pantheon-consolidate.md +1 -1
- package/commands/pantheon-deepwork.md +14 -3
- package/commands/pantheon-focus.md +17 -0
- package/commands/pantheon-optimize.md +2 -2
- package/commands/pantheon-vision.md +61 -12
- package/package.json +30 -11
- package/plugin.json +2 -2
- package/pyproject.toml +11 -16
- package/scripts/build-agents-md.mjs +376 -58
- package/scripts/changelog-merge.mjs +161 -0
- package/scripts/check-plugin-v2-coverage.mjs +213 -0
- package/scripts/ci-validate-yaml.py +7 -5
- package/scripts/{code_mode_server.py → code_mode.py} +119 -27
- package/scripts/cost.mjs +244 -41
- package/scripts/doctor.mjs +236 -29
- package/scripts/install/agents-md.mjs +4 -4
- package/scripts/install/cli-ui.mjs +1 -1
- package/scripts/install/config-migration.mjs +319 -6
- package/scripts/install/health-check.mjs +26 -37
- package/scripts/install/interactive.mjs +7 -7
- package/scripts/install/migrate.mjs +1 -1
- package/scripts/install/opencode-version.mjs +246 -4
- package/scripts/install/opencode.mjs +627 -179
- package/scripts/install/plugin.mjs +43 -9
- package/scripts/install/shared.mjs +120 -12
- package/scripts/install/vision-prompt.mjs +161 -0
- package/scripts/install-mcp.mjs +28 -9
- package/scripts/{mcp_persistence_server.py → mcp_persistence.py} +392 -78
- package/{src/mcp/mcp_resources_server.py → scripts/mcp_resources.py} +2 -2
- package/scripts/{memory_mcp_server.py → memory_mcp.py} +372 -198
- package/scripts/postinstall.mjs +0 -7
- package/scripts/probe-context-rehydrate.mjs +36 -13
- package/scripts/probe-pantheon-cost.mjs +71 -15
- package/scripts/prune.mjs +2 -2
- package/scripts/run-node-tests.mjs +189 -0
- package/scripts/sync-artifacts.mjs +26 -13
- package/scripts/sync-tui.mjs +24 -3
- package/scripts/test-opencode-v2-sandbox.sh +1913 -0
- package/scripts/validate-routing.mjs +43 -3
- package/scripts/validate-sandbox-install.mjs +138 -0
- package/scripts/versioning.mjs +13 -1
- package/scripts/vision-status.mjs +682 -0
- package/skills-lock.json +15 -16
- package/src/agents/aphrodite.md +20 -37
- package/src/agents/apollo.md +7 -3
- package/src/agents/athena.md +63 -35
- package/src/agents/demeter.md +8 -28
- package/src/agents/gaia.md +1 -1
- package/src/agents/hephaestus.md +10 -21
- package/src/agents/hermes.md +15 -38
- package/src/agents/iris.md +1 -1
- package/src/agents/mnemosyne.md +23 -60
- package/src/agents/nyx.md +1 -1
- package/src/agents/prometheus.md +16 -20
- package/src/agents/talos.md +10 -2
- package/src/agents/themis.md +5 -5
- package/src/agents/zeus.md +99 -130
- package/src/instructions/agent-return-format.instructions.md +18 -3
- package/src/instructions/backend-standards.instructions.md +2 -0
- package/src/instructions/frontend-standards.instructions.md +3 -1
- package/src/instructions/memory-protocol.instructions.md +11 -110
- package/src/instructions/zeus-anti-stall.instructions.md +14 -61
- package/src/instructions/zeus-communication-rules.instructions.md +0 -1
- package/src/instructions/zeus-council-synthesis.instructions.md +32 -260
- package/src/instructions/zeus-memory-operations.instructions.md +26 -0
- package/src/instructions/zeus-timeout-retry.instructions.md +40 -87
- package/src/mcp/{code_mode_server.py → code_mode.py} +119 -27
- package/src/mcp/eval_store.py +2 -2
- package/src/mcp/mcp_codemap_module.py +559 -129
- package/src/mcp/{mcp_persistence_server.py → mcp_persistence.py} +392 -78
- package/{scripts/mcp_resources_server.py → src/mcp/mcp_resources.py} +2 -2
- package/src/mcp/{memory_mcp_server.py → memory_mcp.py} +373 -250
- package/src/mcp/{pantheon_vision_server.py → pantheon_vision.py} +396 -40
- package/src/mcp/requirements-mcp.txt +10 -6
- package/src/mcp/requirements-vision.txt +4 -2
- package/src/pantheon/cost-command.ts +399 -101
- package/src/pantheon/delegation-enforce.ts +10 -1
- package/src/pantheon/model-command.ts +18 -7
- package/src/pantheon/presets.d.mts +0 -8
- package/src/pantheon/presets.mjs +5 -32
- package/src/pantheon/todo-enforcer.ts +1 -1
- package/src/pantheon/v2-bounded-read.ts +640 -0
- package/src/pantheon/v2-delegation-enforce.ts +214 -0
- package/src/pantheon/v2-execute-after.ts +186 -0
- package/src/pantheon/v2-tools.ts +429 -0
- package/src/pantheon/v2-unsupported.d.mts +11 -0
- package/src/pantheon/v2-unsupported.mjs +90 -0
- package/src/pantheon/v2-vision.ts +622 -0
- package/src/pantheon/vision-local.ts +531 -0
- package/src/pantheon/vision.ts +1161 -116
- package/src/plugin-v2.ts +1244 -288
- package/src/plugin.ts +12 -9
- package/src/plugins/tui/dist/server.d.ts +1 -2
- package/src/plugins/tui/dist/server.d.ts.map +1 -1
- package/src/plugins/tui/dist/tui-v2.d.ts +154 -0
- package/src/plugins/tui/dist/tui-v2.d.ts.map +1 -0
- package/src/plugins/tui/dist/tui-v2.js +659 -0
- package/src/plugins/tui/dist/tui-v2.js.map +1 -0
- package/src/plugins/tui/dist/tui.d.ts +85 -82
- package/src/plugins/tui/dist/tui.d.ts.map +1 -1
- package/src/plugins/tui/dist/tui.js +72 -59
- package/src/plugins/tui/index-v2.ts +6 -0
- package/src/plugins/tui/package-lock.json +5504 -1341
- package/src/plugins/tui/package.json +25 -15
- package/src/plugins/tui/src/index-v2.tsx +734 -0
- package/src/plugins/tui/src/index.tsx +106 -85
- package/src/plugins/tui/src/opencode-v2.d.ts +13 -0
- package/src/plugins/tui/tsconfig.build.json +5 -0
- package/src/plugins/tui/tsconfig.typecheck.json +5 -0
- package/src/plugins/tui/tsdown.config.ts +22 -2
- package/src/plugins/tui/tui-v2.ts +2 -0
- package/src/plugins/tui/tui.ts +3 -0
- package/src/routing.yml +11 -22
- package/src/skills/README.md +34 -39
- package/src/skills/agent-coordination/SKILL.md +7 -2
- package/src/skills/artifact-management/SKILL.md +12 -5
- package/src/skills/auto-continue/SKILL.md +54 -95
- package/src/skills/code-review-checklist/SKILL.md +5 -5
- package/src/skills/context-compression/SKILL.md +26 -879
- package/src/skills/session-goal/SKILL.md +14 -5
- package/src/skills/spec-driven-development/SKILL.md +3 -1
- package/src/skills/tdd-with-agents/SKILL.md +18 -10
- 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/opencode.json +0 -58
- 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
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "Zeus-only council memory procedures"
|
|
3
|
+
name: "Zeus Memory Operations"
|
|
4
|
+
agents: [zeus]
|
|
5
|
+
fallback: shared
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Zeus Memory Operations
|
|
9
|
+
|
|
10
|
+
## Routing
|
|
11
|
+
|
|
12
|
+
Reuse relevant memory already present in the request or task context. If prior project history matters and no result is present, Zeus may do one targeted search and pass useful hits with the delegation. Do not search on every turn or add KV/cache calls to ordinary dispatches.
|
|
13
|
+
|
|
14
|
+
## Council Decisions
|
|
15
|
+
|
|
16
|
+
Before a council, search separately from the task-level lookup only when a past council decision could affect the recommendation:
|
|
17
|
+
`memory_search(query=question, top_k=2, namespace="council_decisions")`.
|
|
18
|
+
Use a recent, directly matching decision only after checking current context;
|
|
19
|
+
otherwise treat it as background, never as authority to skip a needed review.
|
|
20
|
+
|
|
21
|
+
Persist only reusable, material decisions. Use
|
|
22
|
+
`key="council:<yyyy-mm-dd>:<slug>"`; JSON-stringify both `value` and
|
|
23
|
+
`metadata` for `memory_store(namespace="council_decisions", ...)`. Keep the
|
|
24
|
+
record concise: question, recommendation, specialists, confidence, unresolved
|
|
25
|
+
trade-off, and timestamp. Do not store routine subtask summaries or status
|
|
26
|
+
updates. Use the memory bank for ADRs only when explicitly requested.
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: "Timeout enforcement, retry policies, subtask dispatch, and timeout tracking for Zeus"
|
|
3
3
|
name: "Zeus Timeout & Retry"
|
|
4
|
-
|
|
4
|
+
agents: [zeus]
|
|
5
|
+
fallback: shared
|
|
5
6
|
---
|
|
6
7
|
|
|
7
8
|
# ⏱️ TIMEOUT & RETRY ENFORCEMENT
|
|
@@ -10,123 +11,75 @@ When a delegated agent does not respond in time, enforce the timeout policy from
|
|
|
10
11
|
|
|
11
12
|
## Timeout Behavior by Agent Role
|
|
12
13
|
|
|
13
|
-
| Agent Role | Timeout | Retry Policy |
|
|
14
|
-
|
|
15
|
-
| Explorer (@apollo) | 60s |
|
|
16
|
-
| Implementer (@hermes, @aphrodite, @demeter) | 180s |
|
|
17
|
-
| Reviewer (@themis) | 120s |
|
|
18
|
-
| Infrastructure (@prometheus) | 300s |
|
|
19
|
-
| Hotfix (@talos) | 30s |
|
|
20
|
-
| Remote Sensing (@gaia) | 120s |
|
|
14
|
+
| Agent Role | Timeout | Retry Policy | Partial Results OK? | Reasoning Effort |
|
|
15
|
+
|------------|---------|--------------|---------------------|------------------|
|
|
16
|
+
| Explorer (@apollo) | 60s | 1 retry | ✅ Yes | low |
|
|
17
|
+
| Implementer (@hermes, @aphrodite, @demeter) | 180s | 1 retry | ❌ No | medium |
|
|
18
|
+
| Reviewer (@themis) | 120s | 1 retry | ❌ No | high |
|
|
19
|
+
| Infrastructure (@prometheus) | 300s | 1 retry | ❌ No | medium |
|
|
20
|
+
| Hotfix (@talos) | 30s | 1 retry | ✅ Yes | low |
|
|
21
|
+
| Remote Sensing (@gaia) | 120s | 1 retry | ✅ Yes | high |
|
|
21
22
|
|
|
22
23
|
## Retry Flow
|
|
23
24
|
|
|
24
25
|
```
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
│
|
|
29
|
-
├─ retries exhausted → FALLBACK CHAIN:
|
|
30
|
-
│ ├─ fallback[0] exists? → dispatch to fallback agent
|
|
31
|
-
│ │ └─ keep original task spec + context; add error context
|
|
32
|
-
│ ├─ fallback[1] exists? → dispatch to second fallback
|
|
33
|
-
│ └─ all fallbacks failed? → ESCALATE
|
|
34
|
-
│
|
|
35
|
-
├─ no fallbacks defined → return TIMEOUT error to user
|
|
36
|
-
│
|
|
37
|
-
└─ ESCALATE: "Agent X failed after N retries. Fallbacks Y, Z also failed.
|
|
38
|
-
Options: (a) try different agent, (b) simplify scope, (c) manual"
|
|
26
|
+
Initial attempt → timeout/failure → log it → make at most one retry
|
|
27
|
+
└─ retry fails → try fallback agents left-to-right with original task/context
|
|
28
|
+
└─ all fallbacks fail (or none exist) → report the chain and escalate
|
|
39
29
|
```
|
|
30
|
+
`background_delegation.retry_count: 1` means one retry after the initial attempt, not one total attempt. Never restart an exhausted chain automatically.
|
|
40
31
|
|
|
41
|
-
|
|
32
|
+
Apply the retry only to a transient dispatch failure (timeout, crash, or empty response). A failed test, validation, or migration is a result to diagnose, not a reason to repeat the same command or agent blindly. Track agents already tried: each agent may be retried at most once and each listed fallback may be tried at most once. For auth/security/data/schema work, a fallback must preserve specialist competence, Themis review, and human approval; otherwise stop and escalate. Never let full-auto bypass these gates.
|
|
33
|
+
An agent refusal or scope-boundary response is not a transient failure: do not retry it with a rephrased prompt. Correct the route once if the right specialist is clear; otherwise stop and ask.
|
|
42
34
|
|
|
43
35
|
## Fallback Chain Definitions
|
|
44
36
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
| @apollo | @athena (plan scoped task) | @hermes (implement search) | @zeus |
|
|
50
|
-
| @hermes | @talos (minimal fix) | @athena (replan + simplify) | @zeus |
|
|
51
|
-
| @aphrodite | @talos (CSS/UX fix) | @hermes (generic fallback) | @zeus |
|
|
52
|
-
| @demeter | @hermes (generic backend) | @athena (replan schema) | @zeus |
|
|
53
|
-
| @themis | @zeus (direct escalation) | — | user |
|
|
54
|
-
| @prometheus | @hermes (config/deploy) | @zeus | user |
|
|
55
|
-
| @talos | @hermes (full implementation) | — | @zeus |
|
|
56
|
-
| @hephaestus | @nyx (observability debug) | @hermes (generic) | @zeus |
|
|
57
|
-
| @nyx | @hermes (generic) | — | @zeus |
|
|
58
|
-
| @iris | @zeus (manual override) | — | user |
|
|
59
|
-
| @mnemosyne | @zeus (manual) | — | user |
|
|
37
|
+
`routing.yml` → `fallback_chains` is the canonical agent-by-agent mapping. Evaluate
|
|
38
|
+
the configured chain left-to-right; if an agent has no configured chain, stop
|
|
39
|
+
and escalate rather than guessing a fallback. Each listed fallback may be tried
|
|
40
|
+
at most once, and an exhausted chain is never restarted automatically.
|
|
60
41
|
|
|
61
|
-
###
|
|
62
|
-
When ALL fallbacks fail:
|
|
63
|
-
1. Log the full failure chain: which agents were tried, what error each returned
|
|
64
|
-
2. Report to user: "Task [X] failed. Tried: [agent list]. Error: [summary]."
|
|
65
|
-
3. Offer options: (a) try different approach, (b) simplify scope, (c) manual fix
|
|
66
|
-
4. NEVER retry the same chain automatically — break the cycle
|
|
42
|
+
### Refusal Is a Routing Error
|
|
67
43
|
|
|
68
|
-
|
|
69
|
-
|
|
44
|
+
For a domain/scope refusal, do not retry with a rephrased prompt. Correct the
|
|
45
|
+
route once only when the right specialist is clear; otherwise stop and ask.
|
|
46
|
+
Record one `DelegationCacheDecision` telemetry item for the corrected route,
|
|
47
|
+
including `reroute_from`, `reroute_to`, and `delegation_id` when available.
|
|
70
48
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
Files already explored: backend/routers/auth.py, backend/services/auth_service.py.
|
|
74
|
-
New task: add refresh token rotation.
|
|
75
|
-
```
|
|
49
|
+
### Escalation Protocol
|
|
50
|
+
When all fallbacks fail, report the tried agents and errors, offer a different approach/simpler scope/manual fix, and stop. Never retry the same chain automatically.
|
|
76
51
|
|
|
77
|
-
|
|
52
|
+
### Session Reuse Check
|
|
53
|
+
Check for a reusable session before dispatch and obey `session_max` in `routing.yml`.
|
|
78
54
|
|
|
79
55
|
---
|
|
80
56
|
|
|
81
57
|
# 📦 SUBTASK DISPATCH (Lightweight Delegation)
|
|
82
58
|
|
|
83
|
-
Subtask is a bounded, low-risk delegation mode that **skips** the standard artifact lifecycle.
|
|
59
|
+
Subtask is a bounded, low-risk delegation mode that **skips** the standard artifact lifecycle. Choose it by risk and need for review, not by a fixed file/line count.
|
|
84
60
|
|
|
85
61
|
## When to Use Subtask vs Full Task
|
|
86
62
|
|
|
87
|
-
|
|
63
|
+
Use the lightest path that safely meets the request. A multi-file but bounded low-risk change does not automatically need a plan/artifact phase.
|
|
88
64
|
|
|
89
65
|
### Subtask Decision Tree (run BEFORE every delegation)
|
|
90
66
|
|
|
91
67
|
```
|
|
92
|
-
□
|
|
93
|
-
□
|
|
94
|
-
□
|
|
95
|
-
□ Data: No data loss risk, no migration? [YES→continue | NO→full task]
|
|
96
|
-
□ Review: Output does NOT feed into Themis review? [YES→continue | NO→full task]
|
|
97
|
-
|
|
98
|
-
ALL YES → subtask (skip artifact + Themis)
|
|
99
|
-
ANY NO → full task (IMPL artifact + Themis review mandatory)
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
### Comparison Table
|
|
68
|
+
□ Is the scope clear, bounded, and reversible? [YES→continue | NO→clarify/plan]
|
|
69
|
+
□ Does it avoid auth/security, data/schema, and destructive risk? [YES→continue | NO→full review/gates]
|
|
70
|
+
□ Is there no required Themis/audit handoff for this change? [YES→continue | NO→preserve that review]
|
|
103
71
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
| Risk | Low (no security/data implications) | Any risk level |
|
|
108
|
-
| Artifact | ❌ No IMPL artifact | ✅ IMPL artifact required |
|
|
109
|
-
| Themis review | ❌ None | ✅ Mandatory |
|
|
110
|
-
| Use case | Apollo discovery, Talos hotfix, bounded fix | Feature implementation, migration, API change |
|
|
111
|
-
|
|
112
|
-
### Concrete Examples
|
|
113
|
-
|
|
114
|
-
| Task | Scope | Risk | Subtask? | Why |
|
|
115
|
-
|------|-------|------|----------|-----|
|
|
116
|
-
| Fix typo in CSS class | 1 file, 1 line | None | ✅ | Bounded, no security impact |
|
|
117
|
-
| Add error handling to existing endpoint | 1 file, 5 lines | Low | ✅ | No schema change |
|
|
118
|
-
| Implement login endpoint | 2+ files, 50+ lines | High (auth) | ❌ | Security-critical, needs Themis |
|
|
119
|
-
| Database migration | 1 file, 15 lines | High (data) | ❌ | Data loss risk, needs rollback |
|
|
120
|
-
| Apollo codebase search | 0 files | None | ✅ | Read-only investigation |
|
|
121
|
-
| Update README | 1 file, 3 lines | None | ✅ | Documentation only |
|
|
72
|
+
ALL YES → direct/lightweight execution; no routine plan or artifact
|
|
73
|
+
ANY NO → add only the planning, artifact, specialist review, and approval gates the risk requires
|
|
74
|
+
```
|
|
122
75
|
|
|
123
76
|
### Safety Rules
|
|
124
|
-
1. **Bounded scope** —
|
|
77
|
+
1. **Bounded scope** — can be a small multi-file change or a read-only investigation
|
|
125
78
|
2. **Low risk** — no security implications, no data loss, no breaking changes
|
|
126
|
-
3. **No Themis dependency** — output
|
|
79
|
+
3. **No required Themis dependency** — sensitive/material output retains its review gate
|
|
127
80
|
|
|
128
81
|
## Subtask Return Format
|
|
129
|
-
Return the
|
|
82
|
+
Return the required `## subtask_summary` fields defined in `## Agent Return Format`; include `memory_context` when memory was used.
|
|
130
83
|
|
|
131
84
|
## Timeout Parcial (Partial Results)
|
|
132
85
|
|
|
@@ -5,12 +5,12 @@ Provides a confined execution environment for orchestration scripts
|
|
|
5
5
|
via MCP tools and resources.
|
|
6
6
|
|
|
7
7
|
Usage:
|
|
8
|
-
python scripts/
|
|
8
|
+
python scripts/code_mode.py
|
|
9
9
|
|
|
10
10
|
Or via MCP client (stdio transport):
|
|
11
11
|
pantheon-code-mode:
|
|
12
12
|
command: python
|
|
13
|
-
args: ["scripts/
|
|
13
|
+
args: ["scripts/code_mode.py"]
|
|
14
14
|
"""
|
|
15
15
|
|
|
16
16
|
from __future__ import annotations
|
|
@@ -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
|
|
@@ -31,7 +36,7 @@ from typing import Any
|
|
|
31
36
|
|
|
32
37
|
import yaml
|
|
33
38
|
from _pantheon_paths import pantheon_home, pantheon_project
|
|
34
|
-
from
|
|
39
|
+
from fastmcp import FastMCP
|
|
35
40
|
|
|
36
41
|
# ── Constants ─────────────────────────────────────────────────────────────────
|
|
37
42
|
ALLOWED_EXTENSIONS: frozenset[str] = frozenset({".sh", ".py"})
|
|
@@ -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,
|
package/src/mcp/eval_store.py
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"""Plugin eval certification storage — ``plugin_eval`` memory namespace.
|
|
3
3
|
|
|
4
4
|
Thin stdlib-only wrapper around the memory SQLite DB (same schema as
|
|
5
|
-
``src/mcp/
|
|
5
|
+
``src/mcp/memory_mcp.py``) for plugin-eval certification results.
|
|
6
6
|
|
|
7
7
|
Namespace: ``plugin_eval``
|
|
8
8
|
Key format: ``eval:<name>:<date>`` (e.g. ``eval:tdd-with-agents:2026-08-21``)
|
|
@@ -30,7 +30,7 @@ from typing import Any
|
|
|
30
30
|
|
|
31
31
|
NAMESPACE = "plugin_eval"
|
|
32
32
|
|
|
33
|
-
# Minimal schema — matches
|
|
33
|
+
# Minimal schema — matches memory_mcp.SCHEMA_SQL for the memories
|
|
34
34
|
# table (vec/FTS tables are initialized by the memory MCP server on first
|
|
35
35
|
# run; eval entries remain readable via list/recall regardless).
|
|
36
36
|
_SCHEMA_SQL = """
|