feat: workspace injection contract — entrypoint section 5.5 + WorkspaceFiles helper + docs#168
feat: workspace injection contract — entrypoint section 5.5 + WorkspaceFiles helper + docs#168NeuralEmpowerment wants to merge 17 commits into
Conversation
Captures the agentic-primitives entrypoint contract for inbound file injection (CLAUDE.md, plugins, loose subagents) the workspace image exposes to any orchestrator. Frames the full workspace responsibility as inject/isolate/observe — this spec extends inject; isolate and observe are status quo. Key decisions captured: - Bind-mount at /etc/agentic/workspace/ (read-only) as the universal inbound seam. - Three optional env vars: AGENTIC_WORKSPACE_CONTEXT/_PLUGINS/_AGENTS. - No AGENTIC_WORKSPACE_ALLOWED_TOOLS — tool restrictions live inside subagent frontmatter or plugin permissions, not as a separate env-var concept. - Three entrypoint actions: copy CLAUDE.md, copy + flag plugins, copy loose subagents. - Plugin-bundled subagents come for free via Claude's --plugin-dir auto-discovery; no extra entrypoint step. - Python WorkspaceFiles helper exposes bind_mount + inject primitives for orchestrators that prefer library import. Phasing: env-var rename in agentic-domain-runner first (AGENTIC_DOMAIN_* → AGENTIC_WORKSPACE_*), then entrypoint, then helper, then image release. Sibling spec (already merged in agentic-domain-runner) referenced for the consumer-side view. Also includes the original handoff doc that started this brainstorming (docs/handoff-workspace-files-primitive.md).
Self-review revisions to 2026-05-12-workspace-injection-contract-design: §5 — entrypoint script: - Extract path/default constants to readonly vars at the top so each path literal appears once (WS_MOUNT, WS_MOUNT_PLUGINS, WS_TARGET_PLUGINS, WS_DEFAULT_CONTEXT, WS_PLUGIN_MANIFEST, etc.) - Pull the duplicated 'filter by env list OR discover all' pattern into a __ws_names helper used by both the plugin and subagent actions - Action bodies become tight read-loops keyed off the helper's name stream - Brief 'why this shape' note explaining the choices §11 — documentation deliverables (NEW): - 11.1 docs/workspace.md as canonical workspace reference - 11.2 README 'Workspace' section signposting to docs/workspace.md - 11.3 ADR-035 capturing durable decisions (035 is the next free number after 034) - 11.4 sibling-repo doc sync in agentic-domain-runner as part of Phase A (env rename) cspell.json added with project-specific vocabulary (agentic, tmpfs, frontmatter, homelab, neuralempowerment, dataclass/dataclasses, pathlib, Pytest, sdlc, Syntropic, etc.) to clear the spec's IDE diagnostic noise.
…ket)
WS in agentic-domain-runner is a real WebSocket concept (/v0/conversations/{cid}/stream
upgrades to WS). Avoid the collision in the workspace entrypoint script by
using INJECT_ as the prefix for path/default constants instead. Captures
the intent of the section (file injection) without ambiguity.
Also gitignore .claude/scheduled_tasks.lock and .claude/settings.local.json
(local-only artifacts that shouldn't be tracked).
5 phases, ~15 tasks with TDD steps:
A. Env-var rename in agentic-domain-runner (AGENTIC_DOMAIN_* →
AGENTIC_WORKSPACE_*, /etc/agentic/domain/ → /etc/agentic/workspace/,
AGENTIC_ALLOWED_TOOLS removed)
B. Entrypoint section 5.5 + 6 integration tests against the built image
C. WorkspaceFiles Python helper (bind_mount + inject) + 3 unit tests +
export
D. docs/workspace.md canonical reference + README Workspace section +
ADR-035
E. Image build/tag + runner pickup + previously-blocked live smoke
Each task has exact file paths, runnable commands, and the actual code
to write. Spec coverage and type-consistency checked in self-review
notes at the end.
Implements spec §5 — file injection from a bind-mounted
/etc/agentic/workspace/ into the agent-visible workspace.
When the bind-mount is present, copies:
- CLAUDE.md → /workspace/CLAUDE.md (verbatim)
- plugins/<name>/ → /workspace/.agentic-plugins/<name>/, appending
--plugin-dir to AGENTIC_PLUGIN_FLAGS (existing baked-in plugins
stay intact)
- agents/<name>.md → ~/.claude/agents/<name>.md (loose subagents;
plugin-bundled subagents load automatically via --plugin-dir)
First integration test (test_entrypoint_copies_workspace_context_md)
covers the CLAUDE.md path. Remaining tests come in the next commit.
Six tests against the built workspace image: - test_entrypoint_copies_workspace_context_md - test_entrypoint_copies_workspace_plugins - test_entrypoint_copies_loose_subagents - test_entrypoint_filters_plugins_by_env - test_entrypoint_skips_when_no_workspace_mount - test_entrypoint_skips_invalid_plugin_dir - test_entrypoint_appends_to_agentic_plugin_flags_does_not_replace Mirrors the existing tests/integration/test_entrypoint_lsp_settings.py pattern (docker run --rm with tmpfs home + optional bind-mounts + env).
…uild cmd mismatch) 001 — LSP entrypoint tests fail because the entrypoint prints discovery logs on stdout, polluting the JSON the tests expect. Pre-existing; fix is to redirect [entrypoint] log lines to stderr. 002 — Plan referenced docker build providers/workspaces/claude-cli but the canonical command is uv run scripts/build-provider.py claude-cli. Subagent worked around it; capturing so the plan and docs/workspace.md get the right command before Phase E.
New module agentic_isolation.workspace_files implementing spec §6.
bind_mount(host, ctr, read_only) -> docker.types.Mount
Host-resident static content. Resolves relative paths to absolute.
inject(container_id, ctr_path, content: bytes) -> None
Generated / remote-fetched content. Streams a single-file tar
archive via docker.put_archive(). Works after create_container,
before start_container — and against remote daemons / K8s.
Three unit tests cover the Mount descriptor shape, relative-path
resolution, and the put_archive call shape with a mocked client.
Three documentation deliverables for the workspace-injection-contract
plus a fresh-agent-session breadcrumb at the top of CLAUDE.md so new
sessions land on docs/workspace.md → ADR-035 → entrypoint.sh without
spelunking.
- docs/workspace.md: canonical workspace reference (~150 lines).
Three responsibilities (inject/isolate/observe), bind-mount layout,
env-var contract, what the agent sees, observe surface, Python
helper usage example, build commands, pointers.
- docs/adrs/035-workspace-injection-contract.md: durable decision
record. Context, decision, four alternatives considered, positive +
negative + neutral consequences, implementation pointers.
Cross-links design spec + plan + sibling runner spec.
- README.md: tight 'Workspace' section after Docker Workspace Images,
signposting docs/workspace.md and ADR-035. ADR list also updated
to include ADR-035.
Final reviewer caught one stale reference and two non-blocking cosmetic observations: - docs/superpowers/specs/2026-05-12-workspace-injection-contract-design.md:186 said '__ws_names helper' but the implementation was renamed to '__inject_names' in commit 822e706. One-line fix. - docs/issues/003-workspace-injection-cosmetic-followups.md captures two low-priority items for later: defer mkdir of /workspace/.agentic-plugins/ until at least one plugin is actually copied; add a docstring note to WorkspaceFiles.inject() about put_archive's parent-dir requirement. Reviewer verdict was APPROVED with these as optional follow-ups; the PR is ready to merge.
CI's QA → Python Isolation → Check formatting step failed on this file. Phase C subagent added the export test but didn't run the formatter afterward. One-line whitespace fix.
There was a problem hiding this comment.
Pull request overview
This PR introduces a cross-orchestrator “workspace injection contract” for the agentic-workspace-claude-cli image: orchestrators can provide context, plugins, and subagents via a fixed bind-mount location plus a small set of env vars, and the workspace entrypoint composes these into agent-visible locations before executing the command.
Changes:
- Adds entrypoint section 5.5 to copy injected context/plugins/subagents from
/etc/agentic/workspace/into/workspace/and~/.claude/agents/, while appending plugin flags. - Adds a Python
WorkspaceFileshelper (bind-mount + archive injection primitives) and accompanying tests/exports. - Adds canonical documentation (workspace doc + ADR + README section) and integration tests validating the contract.
Reviewed changes
Copilot reviewed 18 out of 19 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
providers/workspaces/claude-cli/scripts/entrypoint.sh |
Implements the entrypoint-side workspace injection/composition behavior (section 5.5). |
tests/integration/test_entrypoint_workspace_injection.py |
Integration tests covering context/plugin/subagent copy behavior and env-var filtering. |
lib/python/agentic_isolation/agentic_isolation/workspace_files.py |
Adds WorkspaceFiles helper for orchestrators to stage files via bind-mount or tar injection. |
lib/python/agentic_isolation/agentic_isolation/__init__.py |
Exports WorkspaceFiles from the package root. |
lib/python/agentic_isolation/tests/test_workspace_files.py |
Unit tests for WorkspaceFiles. |
lib/python/agentic_isolation/tests/test_package_exports.py |
Verifies WorkspaceFiles is exported. |
docs/workspace.md |
Canonical workspace contract reference and usage examples. |
docs/adrs/035-workspace-injection-contract.md |
ADR documenting the decisions behind the injection contract. |
README.md |
Adds a top-level “Workspace” section pointing to canonical docs/ADR/source. |
docs/superpowers/specs/2026-05-12-workspace-injection-contract-design.md |
Design spec describing the contract and rationale in detail. |
docs/superpowers/plans/2026-05-12-workspace-injection-contract.md |
Implementation plan for rolling out the contract across repos. |
docs/issues/README.md |
Introduces a lightweight “issues” note format for follow-ups. |
docs/issues/001-lsp-entrypoint-test-stdout-pollution.md |
Captures a pre-existing integration-test issue for later resolution. |
docs/issues/002-build-command-mismatch-in-plan.md |
Captures an incorrect build command in the plan for later correction. |
docs/handoff-workspace-files-primitive.md |
Adds a handoff doc describing earlier runner-driven staging expectations. |
CLAUDE.md |
Adds a breadcrumb pointing to the new workspace documentation and ADR. |
cspell.json |
Adds spellchecker configuration/wordlist. |
.gitignore |
Ignores additional local Claude config artifacts. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| ctx_src="${INJECT_MOUNT}/${AGENTIC_WORKSPACE_CONTEXT:-${INJECT_DEFAULT_CONTEXT}}" | ||
| if [ -f "${ctx_src}" ]; then | ||
| cp "${ctx_src}" "${INJECT_TARGET_CONTEXT}" | ||
| chmod 644 "${INJECT_TARGET_CONTEXT}" |
| [ -f "${src}/${INJECT_PLUGIN_MANIFEST}" ] || continue | ||
| cp -a "${src}" "${INJECT_TARGET_PLUGINS}/${plugin}" | ||
| AGENTIC_PLUGIN_FLAGS="${AGENTIC_PLUGIN_FLAGS} --plugin-dir ${INJECT_TARGET_PLUGINS}/${plugin}" |
| import json | ||
| import subprocess | ||
| import tempfile |
| import pytest | ||
|
|
||
|
|
| container = client.containers.create(image, mounts=[mount], ...) | ||
|
|
||
| # Inject mode (generated content) | ||
| container = client.containers.create(image, ...) |
| - [ ] **Step 3: Rebuild the workspace image** | ||
|
|
||
| ```bash | ||
| docker build -t agentic-workspace-claude-cli:latest providers/workspaces/claude-cli |
| > record. The sibling consumer (the agentic-domain-runner) is at | ||
| > `/Users/neural/Code/HomeLab/agentic-domain-runner`. |
| ``container.start()`` — the put_archive API requires the container | ||
| to exist but works regardless of running state. | ||
| """ | ||
| target = Path(container_path) |
| ### 3. The workspace entrypoint contract | ||
|
|
||
| The runner already bind-mounts the per-domain dir at `/etc/agentic/domain/` and exports a few env vars. The entrypoint must compose the runner's per-domain files into `/workspace/` so Claude's path-safety heuristic doesn't block them. **This is the missing piece blocking agentic-domain-runner's homelab smoke from passing end-to-end.** | ||
|
|
||
| ## What lands in agentic-primitives | ||
|
|
||
| ### `providers/workspaces/claude-cli/scripts/entrypoint.sh` | ||
|
|
||
| After the existing plugin discovery block, add (specified verbatim in [agentic-domain-runner spec §8.2](https://gitea.neuralempowerment.xyz/HomeLab/agentic-domain-runner/src/branch/feat/per-domain-context-injection/docs/superpowers/specs/2026-05-12-per-domain-context-injection-design.md) and [ADR-013](https://gitea.neuralempowerment.xyz/HomeLab/agentic-domain-runner/src/branch/feat/per-domain-context-injection/docs/adrs/013-per-task-docker-volume.md)): | ||
|
|
||
| ```bash | ||
| # ----------------------------------------------------------------------------- | ||
| # Per-domain context composition (agentic-domain-runner integration) | ||
| # ----------------------------------------------------------------------------- | ||
| # The orchestrator bind-mounts the domain's directory at /etc/agentic/domain | ||
| # read-only and sets AGENTIC_DOMAIN_CONTEXT + AGENTIC_DOMAIN_PLUGINS + | ||
| # AGENTIC_ALLOWED_TOOLS. Compose the agent-visible /workspace/CLAUDE.md | ||
| # (preamble + domain content) and copy plugin trees into /workspace/.agentic-plugins/. | ||
|
|
CI's QA → Python Isolation → Lint failed on two issues:
1. workspace_files.py:45 had quoted forward refs ('docker.types.Mount')
when the underlying type is runtime-imported inside the method —
ruff RUF066 prefers unquoted when possible. Now uses 'from docker
import types as docker_types' at module level and references the
real type without quotes.
2. tests/test_workspace_files.py imported pytest unnecessarily
(F401). Tests use plain functions, not pytest fixtures from the
import.
Auto-fixed via 'uv run ruff check --fix .'. 174 tests still pass.
9 review comments, all addressed:
entrypoint.sh:
- /workspace/CLAUDE.md is now chmod 600 (was 644) — orchestrators may
embed credentials or private guidance; matches the mode used for
~/.claude/settings.json and ~/.git-credentials earlier in the script.
- Plugin copy is now idempotent across re-runs against a persistent
/workspace volume. Without the rm-first the 'cp -a src dst' pattern
against an existing dst/ creates a nested dst/<basename>/ tree.
tests/integration/test_entrypoint_workspace_injection.py:
- Removed unused 'json' and 'tempfile' imports.
lib/python/agentic_isolation/agentic_isolation/workspace_files.py:
- inject() now validates container_path is absolute and has a
non-empty basename, raising ValueError otherwise. Was silently
producing tar entries with empty/invalid filenames for paths like
'/' or 'relative/path'.
- Two new unit tests cover the rejection paths.
docs/workspace.md:
- Fixed Python snippet — was mixing docker_client + client variable
names; copy-paste-runnable now.
docs/superpowers/plans/2026-05-12-workspace-injection-contract.md:
- Replaced the two 'docker build providers/workspaces/claude-cli'
invocations with the canonical 'just build-workspace-claude-cli'
(docs/issues/002 had already noted this).
CLAUDE.md:
- Removed the absolute /Users/neural/... path; replaced with a link
to the sibling repo's Gitea URL.
docs/handoff-workspace-files-primitive.md:
- Deleted. The original handoff doc that kicked off this brainstorming
described the OLD per-domain contract (/etc/agentic/domain,
AGENTIC_DOMAIN_*, AGENTIC_ALLOWED_TOOLS, entrypoint preamble
templating). The merged spec + ADR-035 + docs/workspace.md
supersede it. Git history preserves it.
176 Python tests + 7 integration tests + 1 OpenAPI snapshot all green.
Adds an entry to CHANGELOG.md '## [Unreleased]' summarizing the workspace-injection-contract work: entrypoint section 5.5, WorkspaceFiles Python helper, canonical docs/workspace.md + ADR-035, 12 new tests (7 integration + 5 unit), and the docs/issues/ convention. Notes the backwards-compat behavior, the deliberate choice to keep tool restrictions out of the workspace env-var contract, and the sibling agentic-domain-runner branch with the AGENTIC_WORKSPACE_* rename.
|
|
||
| `agentic-primitives` ships the workspace image — the controlled boundary every AI agent runs inside. The workspace has three responsibilities: | ||
|
|
||
| 1. **Inject** orchestrator-supplied context (`CLAUDE.md`, plugins, subagents) via a bind-mount at `/etc/agentic/workspace/` + three optional env vars (`AGENTIC_WORKSPACE_CONTEXT` / `_PLUGINS` / `_AGENTS`). |
| # Bind-mount mode (host-resident content) | ||
| mount = wf.bind_mount(workspace_dir, "/etc/agentic/workspace", read_only=True) | ||
| container = client.containers.create(image, mounts=[mount], ...) | ||
|
|
||
| # Inject mode (generated content) | ||
| container = client.containers.create(image, ...) | ||
| wf.inject(container.id, "/etc/agentic/workspace/CLAUDE.md", composed_bytes) | ||
| container.start() | ||
| ``` |
| if [ -d "${INJECT_MOUNT}" ]; then | ||
| ctx_src="${INJECT_MOUNT}/${AGENTIC_WORKSPACE_CONTEXT:-${INJECT_DEFAULT_CONTEXT}}" | ||
| if [ -f "${ctx_src}" ]; then | ||
| cp "${ctx_src}" "${INJECT_TARGET_CONTEXT}" | ||
| # 600 because orchestrators may embed credentials or | ||
| # private guidance in the workspace context. Matches the mode | ||
| # used for ~/.claude/settings.json and ~/.git-credentials above. | ||
| chmod 600 "${INJECT_TARGET_CONTEXT}" | ||
| fi | ||
|
|
||
| if [ -d "${INJECT_MOUNT_PLUGINS}" ]; then | ||
| mkdir -p "${INJECT_TARGET_PLUGINS}" | ||
| while IFS= read -r plugin; do | ||
| [ -n "${plugin}" ] || continue | ||
| src="${INJECT_MOUNT_PLUGINS}/${plugin}" | ||
| [ -f "${src}/${INJECT_PLUGIN_MANIFEST}" ] || continue |
|
|
||
| Raises ``ValueError`` if ``container_path`` is not an absolute | ||
| path or has an empty basename (e.g. ``/`` or trailing slash). | ||
| """ |
| ctx_src="${INJECT_MOUNT}/${AGENTIC_WORKSPACE_CONTEXT:-${INJECT_DEFAULT_CONTEXT}}" | ||
| if [ -f "${ctx_src}" ]; then | ||
| cp "${ctx_src}" "${INJECT_TARGET_CONTEXT}" | ||
| chmod 644 "${INJECT_TARGET_CONTEXT}" |
Five new comments from Copilot's second review:
README.md:
- Spelled out env var names (AGENTIC_WORKSPACE_PLUGINS / _AGENTS)
instead of the abbreviated /_PLUGINS / _AGENTS form that could
cause copy-paste misconfiguration.
docs/workspace.md:
- inject() example now targets /workspace/CLAUDE.md (parent
guaranteed by the image) instead of /etc/agentic/workspace/...
which only exists when the orchestrator bind-mounts it. Added
a comment explaining why.
providers/workspaces/claude-cli/scripts/entrypoint.sh:
- Security fix: __inject_safe_filter rejects plugin/agent names
containing '/' or '..'. Previously a value like
AGENTIC_WORKSPACE_PLUGINS='../etc' could escape the intended
/etc/agentic/workspace/plugins/ mount.
lib/python/agentic_isolation/agentic_isolation/workspace_files.py:
- inject() now explicitly rejects trailing slashes; docstring is
accurate. Path('/foo/') normalizes to /foo internally, so the
earlier basename check didn't actually catch this.
- Renamed test_inject_rejects_empty_basename to
test_inject_rejects_root_path since the trailing-slash check
now catches '/' first.
- New test_inject_rejects_trailing_slash.
docs/superpowers/specs/2026-05-12-workspace-injection-contract-design.md:
- Spec snippet was showing chmod 644 but impl uses 600 (the change
we made for round 1). Synced spec → 600 to remove drift.
Tests:
- 177 Python (+1 for trailing-slash test)
- 7 integration green
- ruff check + format clean
- Image rebuilt and integration tests passed against fresh image.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 16 out of 17 changed files in this pull request and generated 6 comments.
Comments suppressed due to low confidence (3)
providers/workspaces/claude-cli/scripts/entrypoint.sh:268
cp -apreserves ownership; since the entrypoint runs as the non-rootagentuser, copying from a bind-mount where files are owned by a different uid/gid can fail with EPERM and abort the entrypoint (becauseset -e). Use a copy mode that does not attempt to preserve ownership (e.g., avoid-aor use--no-preserve=ownership) so workspace injection can't brick container startup.
# rm first → idempotent across re-runs when /workspace is a
# persistent named volume. Without the rm, `cp -a src dst`
# against an existing dst/ creates a nested dst/<basename>/
# tree instead of overwriting.
rm -rf "${INJECT_TARGET_PLUGINS}/${plugin}"
cp -a "${src}" "${INJECT_TARGET_PLUGINS}/${plugin}"
AGENTIC_PLUGIN_FLAGS="${AGENTIC_PLUGIN_FLAGS} --plugin-dir ${INJECT_TARGET_PLUGINS}/${plugin}"
providers/workspaces/claude-cli/scripts/entrypoint.sh:249
AGENTIC_WORKSPACE_CONTEXTis interpolated directly intoctx_srcwithout any traversal guard. Values like../../etc/passwdwould escape/etc/agentic/workspaceand copy an arbitrary in-container file into/workspace/CLAUDE.md. Consider rejecting values containing '/' or '..' (or resolving and asserting the real path stays under the mount) to keep the contract confined to the bind-mount.
if [ -d "${INJECT_MOUNT}" ]; then
ctx_src="${INJECT_MOUNT}/${AGENTIC_WORKSPACE_CONTEXT:-${INJECT_DEFAULT_CONTEXT}}"
if [ -f "${ctx_src}" ]; then
cp "${ctx_src}" "${INJECT_TARGET_CONTEXT}"
CHANGELOG.md:26
- The changelog claims there are "5 unit tests" for
WorkspaceFiles, butlib/python/agentic_isolation/tests/test_workspace_files.pycurrently defines 6 tests (plus the separate export test intest_package_exports.py). Update the counts to match the actual test suite so the release notes stay accurate.
- **7 integration tests** in `tests/integration/test_entrypoint_workspace_injection.py` covering CLAUDE.md copy, plugin copy + flag append, loose subagent copy, env filter, no-mount skip, invalid-plugin skip, plugin-flags append-not-replace.
- **5 unit tests** for `WorkspaceFiles` (descriptor shape, relative-path resolution, `put_archive` call shape, ValueError on non-absolute / empty-basename `container_path`).
- **docs/issues/** convention introduced — numbered enhancement / follow-up notes (003 cosmetic items captured).
| # Reject any name containing '/' or '..' so plugin/agent names supplied | ||
| # via env can't escape the intended mount via path traversal. Caller | ||
| # pipes a stream of names through this filter. | ||
| __inject_safe_filter() { | ||
| while IFS= read -r name; do | ||
| [ -n "${name}" ] || continue | ||
| case "${name}" in | ||
| */*|*..*|"") continue ;; |
| def bind_mount( | ||
| self, | ||
| host_path: Path, | ||
| container_path: str, | ||
| read_only: bool = True, | ||
| ) -> docker.types.Mount: | ||
| """Build a Mount descriptor for `containers.create(mounts=[...])`. | ||
|
|
||
| Relative host_paths are resolved to absolute paths (Docker rejects | ||
| relative bind sources). The descriptor is a plain ``docker.types.Mount`` | ||
| the caller hands to the docker SDK unmodified. | ||
| """ | ||
| from docker.types import Mount | ||
|
|
||
| return Mount( | ||
| target=container_path, | ||
| source=str(Path(host_path).resolve()), | ||
| type="bind", | ||
| read_only=read_only, |
| IMAGE = "agentic-workspace-claude-cli:latest" | ||
|
|
||
|
|
||
| def _run(args: list[str], extra_mounts: list[str] | None = None, env: dict | None = None) -> subprocess.CompletedProcess: | ||
| """Run an arbitrary command in the workspace container with a tmpfs | ||
| home dir (matches LSP test pattern). Optionally bind-mount extra | ||
| paths and pass env vars. Returns the completed process.""" | ||
| cmd = [ | ||
| "docker", "run", "--rm", | ||
| "--tmpfs=/home/agent:rw,exec,nosuid,size=128m,uid=1000,gid=1000", | ||
| ] | ||
| for m in extra_mounts or []: | ||
| cmd.extend(["-v", m]) | ||
| for k, v in (env or {}).items(): | ||
| cmd.extend(["-e", f"{k}={v}"]) | ||
| cmd.append(IMAGE) | ||
| cmd.extend(args) | ||
| return subprocess.run(cmd, capture_output=True, text=True, timeout=60) |
| for the full design see | ||
| [`docs/superpowers/specs/2026-05-12-workspace-injection-contract-design.md`](superpowers/specs/2026-05-12-workspace-injection-contract-design.md). |
| - Design spec: | ||
| [`docs/superpowers/specs/2026-05-12-workspace-injection-contract-design.md`](../superpowers/specs/2026-05-12-workspace-injection-contract-design.md) | ||
| - Plan: | ||
| [`docs/superpowers/plans/2026-05-12-workspace-injection-contract.md`](../superpowers/plans/2026-05-12-workspace-injection-contract.md) |
| - `plugins/<name>/` → `/workspace/.agentic-plugins/<name>/` + appends `--plugin-dir` flags to `AGENTIC_PLUGIN_FLAGS` | ||
| - `agents/<name>.md` → `~/.claude/agents/<name>.md` | ||
| - **`WorkspaceFiles` Python helper** — `lib/python/agentic_isolation/agentic_isolation/workspace_files.py`. Exposes `bind_mount(host, ctr, read_only)` and `inject(container_id, ctr_path, content)` as the two complementary staging primitives. Library import only — no daemon. Exported from `agentic_isolation` package root. | ||
| - **Canonical docs**: [`docs/workspace.md`](docs/workspace.md) describes the workspace's three responsibilities (inject / isolate / observe); [ADR-035](docs/adrs/035-workspace-injection-contract.md) captures the decision; `docs/superpowers/specs/` + `docs/superpowers/plans/` hold the design + implementation plan. |
Summary
Implements the workspace injection contract in agentic-primitives — a small inbound seam every orchestrator (agentic-domain-runner, Syntropic137, future Codex/Gemini wrappers) can target to hand a workspace its context, plugins, and subagents before the agent starts.
What changed
Entrypoint
providers/workspaces/claude-cli/scripts/entrypoint.sh— new section 5.5 (Workspace Context Composition) reads/etc/agentic/workspace/bind-mount + threeAGENTIC_WORKSPACE_*env vars, then copies into/workspace/CLAUDE.md,/workspace/.agentic-plugins/<name>/,~/.claude/agents/<name>.md. Path constantsINJECT_*declared once at the top.Python helper
lib/python/agentic_isolation/agentic_isolation/workspace_files.py—WorkspaceFilesclass withbind_mount()+inject()primitives. Library import only (no daemon). Exported from package root.Docs
docs/workspace.md— canonical workspace reference (inject/isolate/observe responsibility framing)docs/adrs/035-workspace-injection-contract.md— ADR (4 alternatives considered, positive/negative/neutral consequences)docs/superpowers/specs/2026-05-12-workspace-injection-contract-design.md— design specdocs/superpowers/plans/2026-05-12-workspace-injection-contract.md— implementation planREADME.md— focused Workspace section signpostingdocs/workspace.mdCLAUDE.md— breadcrumb at the top for fresh agent sessionsdocs/issues/{001,002}— captured wrinkles (pre-existing LSP test bug, plan's build cmd mismatch)Tests
tests/integration/test_entrypoint_workspace_injection.pyagainst the built image (context copy, plugin copy + flag append, loose subagent copy, env filter, no-mount skip, invalid-plugin skip, plugin-flags append-not-replace)WorkspaceFiles+ 1 export test = 174 Python tests passing (was 170)Coordinated change in
agentic-domain-runnerPhase A — env-var + path rename — landed on branch
feat/workspace-env-rename. Should merge BEFORE this PR's image gets adopted by the runner (Phase E in the plan).What's deliberately NOT here
AGENTIC_WORKSPACE_ALLOWED_TOOLSenv var — tool restrictions live inside subagent frontmatter / plugin permissions (ADR-035 alternative feat(examples): 002-observability-dashboard #3)Test plan
pytest tests/integration/test_entrypoint_workspace_injection.py -v→ 7 passingcd lib/python/agentic_isolation && uv run pytest -v→ 174 passingjust build-workspace-claude-cli), tag a release (next version after 2.1.126), update runner'sexamples/domains/homelab/domain.tomlimage tag, run the previously-blockedlive_claude_sees_domain_claude_mdsmokeSpec / ADR / Plan
docs/superpowers/specs/2026-05-12-workspace-injection-contract-design.mddocs/adrs/035-workspace-injection-contract.mddocs/superpowers/plans/2026-05-12-workspace-injection-contract.mddocs/workspace.md