Issue: Activated skill's subdirectories (scripts/, data/) missing under /workspace/.skills/<name> (docker backend)
Status: Open — NOT caused by the skill all-tool-model branch (feat/agent-runner-plugin). Confirmed via git: the entire mount/register/package chain is byte-identical to origin/master.
Symptom
After an agent creates → registers → activates a multi-file skill, running its script from the activated mount path fails:
exec workdir=/workspace/.skills/lb-e2e-skill cmd="python3 scripts/use.py"
-> exit_code=2 stderr="python3: can't open file '/workspace/.skills/lb-e2e-skill/scripts/use.py': No such file or directory"
Inside the running sandbox container:
/workspace/lb-e2e-skill (where the agent built it) → has SKILL.md + scripts/use.py
/workspace/.skills/lb-e2e-skill (the activate mount point) → empty
So activate succeeds (skill registered, manager.py: Loaded 1 skills, mount point created), but the activated path does not expose the skill's scripts//data/ subdirectories — only the top-level SKILL.md is recognized.
Repro
- local docker backend, host shares filesystem with box (
shares_filesystem_with_box=True).
- Agent:
exec to create /workspace/<skill> with SKILL.md + scripts/use.py.
register_skill path=/workspace/<skill> → box_service.create_skill → Box runtime skill store.
activate skill_name=<skill>.
exec workdir=/workspace/.skills/<skill> cmd="python3 scripts/use.py" → No such file.
Diagnosis (where it breaks)
register_skill (skill_authoring.py) resolves the sandbox dir to a host path and calls skill_service.create_skill({package_root: host_path}) → box_service.create_skill → self.client.create_skill (Box runtime, SDK).
package_root consumed at activation comes from Box runtime list_skills() (manager.py: "Loaded N skills from Box runtime"), not the agent's raw workspace dir.
build_skill_extra_mounts (box/service.py:261) bind-mounts that package_root to /workspace/.skills/<name> — while /workspace itself is already bind-mounted to data/box/default. This nested bind mount (mounting a workspace subdir back into another path inside the same workspace), under docker + shared filesystem, ends up exposing an empty dir.
Two non-exclusive root causes to confirm:
- Box runtime skill store packaging may only persist
SKILL.md/metadata, not the scripts//data/ subtree, so package_root points at an incomplete copy.
- Docker nested bind mount of a path inside an already-bind-mounted
/workspace is masked/empty.
Code ownership (git evidence)
The whole chain is unchanged by feat/agent-runner-plugin:
| layer |
file |
feat vs master |
| mount helpers |
skill.py (get_visible_skills / package_root / parse_skill_mount / resolve_virtual) |
0 changes |
| host skill service |
box/service.py (build_skill_extra_mounts, create_skill) |
unchanged (only box-session-id-template lookup source changed elsewhere) |
| SDK mount exec |
langbot-plugin-sdk/box/backend.py (docker run + -v extra_mounts) |
0 changes |
| SDK skill store |
SDK box/ (Box runtime create_skill / list_skills) |
0 changes |
build_skill_extra_mounts (the nested-mount logic) was introduced in 96b04184 "Feat/sandbox (#2072)" (2026-06-03); later hardened by 8e558ad3 (only added a package_root existence guard). The all-tool-model branch only exposed this path end-to-end for the first time (agent can now reach activate→exec-on-mount), it did not change the mounting code.
Unconfirmed
- Whether skill activation e2e was ever actually run-and-passed on the docker backend before (QA case
sandbox-skill-authoring-e2e.yaml exists, but no pass record found in master's STATUS). The flow may have only been exercised on nsjail/E2B, or this docker+shared-fs nested-mount case may have always failed.
Suggested fix directions
- When host shares filesystem with box, avoid nested bind-mounting
package_root into /workspace/.skills/<name>; instead create a symlink inside the workspace (/workspace/.skills/<name> → the real skill dir), sidestepping docker's masking of nested mounts.
- And/or verify Box runtime's skill store persists the full package (incl.
scripts/, data/), not just SKILL.md.
Reproduction env
- backend=docker, image
rockchin/langbot-sandbox:latest, host_path=data/box/default, mount_path=/workspace, read_only_rootfs=true.
- Pipeline
3e645b04-... (Agent QA Local Agent Debug Chat), model claude-opus-4-8.
Issue: Activated skill's subdirectories (scripts/, data/) missing under
/workspace/.skills/<name>(docker backend)Status: Open — NOT caused by the skill all-tool-model branch (
feat/agent-runner-plugin). Confirmed via git: the entire mount/register/package chain is byte-identical toorigin/master.Symptom
After an agent creates → registers → activates a multi-file skill, running its script from the activated mount path fails:
Inside the running sandbox container:
/workspace/lb-e2e-skill(where the agent built it) → hasSKILL.md+scripts/use.py/workspace/.skills/lb-e2e-skill(the activate mount point) → emptySo
activatesucceeds (skill registered,manager.py: Loaded 1 skills, mount point created), but the activated path does not expose the skill'sscripts//data/subdirectories — only the top-levelSKILL.mdis recognized.Repro
shares_filesystem_with_box=True).execto create/workspace/<skill>withSKILL.md+scripts/use.py.register_skill path=/workspace/<skill>→box_service.create_skill→ Box runtime skill store.activate skill_name=<skill>.exec workdir=/workspace/.skills/<skill> cmd="python3 scripts/use.py"→ No such file.Diagnosis (where it breaks)
register_skill(skill_authoring.py) resolves the sandbox dir to a host path and callsskill_service.create_skill({package_root: host_path})→box_service.create_skill→self.client.create_skill(Box runtime, SDK).package_rootconsumed at activation comes from Box runtimelist_skills()(manager.py: "Loaded N skills from Box runtime"), not the agent's raw workspace dir.build_skill_extra_mounts(box/service.py:261) bind-mounts thatpackage_rootto/workspace/.skills/<name>— while/workspaceitself is already bind-mounted todata/box/default. This nested bind mount (mounting a workspace subdir back into another path inside the same workspace), under docker + shared filesystem, ends up exposing an empty dir.Two non-exclusive root causes to confirm:
SKILL.md/metadata, not thescripts//data/subtree, sopackage_rootpoints at an incomplete copy./workspaceis masked/empty.Code ownership (git evidence)
The whole chain is unchanged by
feat/agent-runner-plugin:skill.py(get_visible_skills / package_root / parse_skill_mount / resolve_virtual)box/service.py(build_skill_extra_mounts,create_skill)box-session-id-templatelookup source changed elsewhere)langbot-plugin-sdk/box/backend.py(docker run +-vextra_mounts)box/(Box runtime create_skill / list_skills)build_skill_extra_mounts(the nested-mount logic) was introduced in96b04184 "Feat/sandbox (#2072)"(2026-06-03); later hardened by8e558ad3(only added apackage_rootexistence guard). The all-tool-model branch only exposed this path end-to-end for the first time (agent can now reach activate→exec-on-mount), it did not change the mounting code.Unconfirmed
sandbox-skill-authoring-e2e.yamlexists, but no pass record found in master's STATUS). The flow may have only been exercised on nsjail/E2B, or this docker+shared-fs nested-mount case may have always failed.Suggested fix directions
package_rootinto/workspace/.skills/<name>; instead create a symlink inside the workspace (/workspace/.skills/<name>→ the real skill dir), sidestepping docker's masking of nested mounts.scripts/,data/), not justSKILL.md.Reproduction env
rockchin/langbot-sandbox:latest,host_path=data/box/default,mount_path=/workspace,read_only_rootfs=true.3e645b04-...(Agent QA Local Agent Debug Chat), model claude-opus-4-8.