Skip to content

feat: workspace injection contract — entrypoint section 5.5 + WorkspaceFiles helper + docs#168

Open
NeuralEmpowerment wants to merge 16 commits into
mainfrom
feat/workspace-injection-contract
Open

feat: workspace injection contract — entrypoint section 5.5 + WorkspaceFiles helper + docs#168
NeuralEmpowerment wants to merge 16 commits into
mainfrom
feat/workspace-injection-contract

Conversation

@NeuralEmpowerment
Copy link
Copy Markdown
Contributor

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 + three AGENTIC_WORKSPACE_* env vars, then copies into /workspace/CLAUDE.md, /workspace/.agentic-plugins/<name>/, ~/.claude/agents/<name>.md. Path constants INJECT_* declared once at the top.

Python helper

  • lib/python/agentic_isolation/agentic_isolation/workspace_files.pyWorkspaceFiles class with bind_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 spec
  • docs/superpowers/plans/2026-05-12-workspace-injection-contract.md — implementation plan
  • README.md — focused Workspace section signposting docs/workspace.md
  • CLAUDE.md — breadcrumb at the top for fresh agent sessions
  • docs/issues/{001,002} — captured wrinkles (pre-existing LSP test bug, plan's build cmd mismatch)

Tests

  • 7 integration tests in tests/integration/test_entrypoint_workspace_injection.py against 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)
  • 3 unit tests for WorkspaceFiles + 1 export test = 174 Python tests passing (was 170)

Coordinated change in agentic-domain-runner

Phase 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

  • No AGENTIC_WORKSPACE_ALLOWED_TOOLS env var — tool restrictions live inside subagent frontmatter / plugin permissions (ADR-035 alternative feat(examples): 002-observability-dashboard #3)
  • No identity vars (task id, runner URL, etc.) — orchestrator-specific, not part of the workspace contract
  • No preamble templating in the entrypoint — orchestrator pre-composes any preamble into the bind-mounted CLAUDE.md before mounting

Test plan

  • pytest tests/integration/test_entrypoint_workspace_injection.py -v → 7 passing
  • cd lib/python/agentic_isolation && uv run pytest -v → 174 passing
  • After merge: build the workspace image (just build-workspace-claude-cli), tag a release (next version after 2.1.126), update runner's examples/domains/homelab/domain.toml image tag, run the previously-blocked live_claude_sees_domain_claude_md smoke

Spec / ADR / Plan

  • Spec: docs/superpowers/specs/2026-05-12-workspace-injection-contract-design.md
  • ADR: docs/adrs/035-workspace-injection-contract.md
  • Plan: docs/superpowers/plans/2026-05-12-workspace-injection-contract.md
  • Canonical doc: docs/workspace.md

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.
Copilot AI review requested due to automatic review settings May 13, 2026 02:10
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.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 WorkspaceFiles helper (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 thread docs/workspace.md
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 thread CLAUDE.md Outdated
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.
Copilot AI review requested due to automatic review settings May 13, 2026 02:39
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.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 18 out of 19 changed files in this pull request and generated 5 comments.

Comment thread README.md Outdated

`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 thread docs/workspace.md
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants