feat: workspace injection contract — entrypoint section 5.5 + WorkspaceFiles helper + docs#168
Open
NeuralEmpowerment wants to merge 16 commits into
Open
feat: workspace injection contract — entrypoint section 5.5 + WorkspaceFiles helper + docs#168NeuralEmpowerment wants to merge 16 commits into
NeuralEmpowerment wants to merge 16 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}" |
Comment on lines
+245
to
+247
| [ -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}" |
Comment on lines
+11
to
+13
| import json | ||
| import subprocess | ||
| import tempfile |
Comment on lines
+9
to
+11
| import pytest | ||
|
|
||
|
|
Comment on lines
+114
to
+117
| 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 |
Comment on lines
+18
to
+19
| > 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) |
Comment on lines
+28
to
+46
| ### 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`). |
Comment on lines
+113
to
+121
| # 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() | ||
| ``` |
Comment on lines
+233
to
+248
| 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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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