space-architect 2.0.0 → 2.0.2
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +37 -0
- data/README.md +26 -4
- data/lib/space_core/cli/run.rb +16 -2
- data/lib/space_core/oci_packer.rb +4 -0
- data/lib/space_core/oci_runner.rb +22 -6
- data/lib/space_core/space.rb +6 -0
- data/lib/space_core/templates/oci/dockerfile.erb +22 -12
- data/lib/space_core/templates/oci/entrypoint.sh.erb +6 -0
- data/lib/space_core/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7251e017d780c7fc4accd3d4ee367d2b5063bb4d0c1692c0c69bc81a228c9fa7
|
|
4
|
+
data.tar.gz: ad25da0cf6025716953685d7483e86898368170967a29465625335337f8427a3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: baf43e94bcd35987bb133f3958709bfc4083851732ed2d47ef72b25b92e042cfb72c2594f1067189e9581c554140cd2d6145dc7d607f9137931250522ba445dc
|
|
7
|
+
data.tar.gz: 0ecd90d05ea7dc4c3832fea90f7af76650b1c8279ced9e7e48242b825aaeef8e7291d6dc9b4dacd6bfac00ad647ecca887a2f27abb3823181287885ccede7210
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,41 @@ All notable changes to this project are documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [2.0.2] - 2026-07-01
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **Declarative runtime env forwarding for `space run`.** A space can declare
|
|
13
|
+
`run.env:` in `space.yaml` — a list of host env vars forwarded into the
|
|
14
|
+
container as bare `-e VAR` passthrough (value never lands in argv/`ps`), on top
|
|
15
|
+
of the always-on substrate auth vars. `space run --env VAR` (repeatable) adds
|
|
16
|
+
more ad hoc. A requested-but-unset var warns on stderr instead of failing
|
|
17
|
+
opaquely in-guest. This lets credentialed payloads (e.g. a Fireworks/OpenAI key)
|
|
18
|
+
run via a plain `space run <cmd>` with no wall of flags, without baking secrets.
|
|
19
|
+
|
|
20
|
+
## [2.0.1] - 2026-07-01
|
|
21
|
+
|
|
22
|
+
Fast-follow completing the `pack.persist:` story from 2.0.0.
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
|
|
26
|
+
- **Seed-on-empty for `pack.persist:`.** Content baked at a persisted path now
|
|
27
|
+
survives a first-run empty bind mount. `space pack` snapshots each persisted
|
|
28
|
+
dir to a `/opt/space-seed<path>` sidecar after provisioning, and the entrypoint
|
|
29
|
+
restores an empty mount from its seed on first run only (a non-empty mount
|
|
30
|
+
keeps its evolving state). Previously the empty host mount shadowed the baked
|
|
31
|
+
content — e.g. a provisioned `~/.hermes/config.yaml` was hidden once
|
|
32
|
+
`/root/.hermes` was persisted. Template-only; a space with no `pack.persist:`
|
|
33
|
+
renders an identical Dockerfile and entrypoint.
|
|
34
|
+
|
|
35
|
+
### Fixed
|
|
36
|
+
|
|
37
|
+
- **First-run boot on bind-mount-root-restricted runtimes.** The seed restore
|
|
38
|
+
tolerates a benign copy failure (`2>/dev/null || true`) so that on runtimes
|
|
39
|
+
where the persisted path is a bind mount whose root rejects utime updates
|
|
40
|
+
(virtiofs under Apple `container`), the entrypoint's `set -e` no longer turns a
|
|
41
|
+
cosmetic `cp` EPERM into a fatal abort before the payload execs.
|
|
42
|
+
|
|
8
43
|
## [2.0.0] - 2026-07-01
|
|
9
44
|
|
|
10
45
|
A ground-up release across four fronts:
|
|
@@ -293,6 +328,8 @@ the **BREAKING** items under Changed. The repository is now a monorepo; the
|
|
|
293
328
|
lanes, worktrees, and headless `claude -p` dispatch, with variant sets and the
|
|
294
329
|
claude-code and opencode harnesses.
|
|
295
330
|
|
|
331
|
+
[2.0.2]: https://github.com/jetpks/space-architect/compare/v2.0.1...v2.0.2
|
|
332
|
+
[2.0.1]: https://github.com/jetpks/space-architect/compare/v2.0.0...v2.0.1
|
|
296
333
|
[2.0.0]: https://github.com/jetpks/space-architect/compare/v1.3.0...v2.0.0
|
|
297
334
|
[1.3.0]: https://github.com/jetpks/space-architect/compare/v1.2.0...v1.3.0
|
|
298
335
|
[1.2.0]: https://github.com/jetpks/space-architect/compare/v1.1.0...v1.2.0
|
data/README.md
CHANGED
|
@@ -158,6 +158,19 @@ variables that are actually set — `ANTHROPIC_API_KEY`, `CLAUDE_CODE_OAUTH_TOKE
|
|
|
158
158
|
`ANTHROPIC_BASE_URL` — with `-e` at run time, so credentials live in your shell,
|
|
159
159
|
never in the image. 🗝️
|
|
160
160
|
|
|
161
|
+
**Forwarding payload credentials.** Beyond the always-on auth trio, a space can declare
|
|
162
|
+
`run.env:` in `space.yaml` — a list of host env var names forwarded into the container at
|
|
163
|
+
run time. You can also pass `space run --env VAR` (repeatable) for ad hoc additions. All
|
|
164
|
+
forwarding is bare `-e VAR` passthrough: values never appear in argv, `ps`, or the image.
|
|
165
|
+
A requested-but-unset var warns on stderr instead of silently failing inside the guest.
|
|
166
|
+
|
|
167
|
+
```yaml
|
|
168
|
+
run:
|
|
169
|
+
env: # run-time: host var names, forwarded as bare -e VAR
|
|
170
|
+
- FIREWORKS_API_KEY
|
|
171
|
+
- OPENAI_API_KEY
|
|
172
|
+
```
|
|
173
|
+
|
|
161
174
|
**Declaring provisioning & persistence** — two optional keys in `space.yaml`:
|
|
162
175
|
|
|
163
176
|
```yaml
|
|
@@ -169,10 +182,19 @@ pack:
|
|
|
169
182
|
- /root/.local/state
|
|
170
183
|
```
|
|
171
184
|
|
|
172
|
-
`provision` scripts must live under the space root and
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
185
|
+
`provision` scripts must live under the space root and be executable. Each script is
|
|
186
|
+
copied into the image individually (`COPY <script> /space/<script>`) and then invoked
|
|
187
|
+
(`RUN /space/<script>`) **before** the full space tree lands and **before** the gem is
|
|
188
|
+
installed. This ordering is the cache-hygiene guarantee: editing any other space file
|
|
189
|
+
leaves the provision and gem-install layers cached, so a rebuild completes in seconds
|
|
190
|
+
instead of minutes. Scripts must therefore be self-contained — they run with only the
|
|
191
|
+
base system layers and any earlier provision scripts' outputs; they cannot read other
|
|
192
|
+
space files or call `architect`/`space`. Scripts must have the executable bit set; COPY
|
|
193
|
+
preserves the mode from the build context.
|
|
194
|
+
|
|
195
|
+
`persist` paths must be absolute; `space run` bind-mounts each one from
|
|
196
|
+
`<space>/.state<path>` on the host (created on first run) so a container's mutable state
|
|
197
|
+
survives across runs. Both are validated at pack time.
|
|
176
198
|
|
|
177
199
|
Like everything else, these are reachable as `architect space pack|build|run`
|
|
178
200
|
from inside a project.
|
data/lib/space_core/cli/run.rb
CHANGED
|
@@ -9,21 +9,35 @@ class Run < BaseCommand
|
|
|
9
9
|
|
|
10
10
|
argument :command, type: :array, required: false, desc: "Command to run in the container (default: login shell)"
|
|
11
11
|
option :tty, type: :boolean, default: nil, desc: "Force interactive TTY (default: auto-detect)"
|
|
12
|
+
option :env, type: :array, desc: "Host env var to forward into the container (repeatable; adds to run.env)"
|
|
12
13
|
|
|
13
|
-
def call(command: [], tty: nil, **opts)
|
|
14
|
+
def call(command: [], tty: nil, env: [], **opts)
|
|
14
15
|
setup_terminal(**opts.slice(:color, :colors))
|
|
15
16
|
handle_errors do
|
|
16
17
|
result = store.current.bind do |space|
|
|
17
|
-
runner = Space::Core::OciRunner.new(
|
|
18
|
+
runner = Space::Core::OciRunner.new(
|
|
19
|
+
space: space, interactive: tty.nil? ? CLI.tty?(out) : tty, env_vars: env
|
|
20
|
+
)
|
|
18
21
|
runner.command(command).fmap { |argv| { argv: argv, runner: runner } }
|
|
19
22
|
end
|
|
20
23
|
render(result) do |r|
|
|
21
24
|
r[:runner].host_dirs.each { |d| FileUtils.mkdir_p(d) }
|
|
25
|
+
warn_missing_env(r[:runner].missing_env)
|
|
22
26
|
terminal.say "Running: #{r[:argv].join(' ')}"
|
|
23
27
|
out.flush # Kernel.exec replaces the process without flushing buffered IO
|
|
24
28
|
Kernel.exec(*r[:argv])
|
|
25
29
|
end
|
|
26
30
|
end
|
|
27
31
|
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
# A requested var absent from the host env is almost always a forgotten export;
|
|
36
|
+
# forwarding silently omits it and the payload fails opaquely in-guest. Warn, don't fail.
|
|
37
|
+
def warn_missing_env(missing)
|
|
38
|
+
return if missing.empty?
|
|
39
|
+
|
|
40
|
+
terminal.error("Warning: requested env not set on host, not forwarded: #{missing.join(', ')}")
|
|
41
|
+
end
|
|
28
42
|
end
|
|
29
43
|
end
|
|
@@ -84,6 +84,10 @@ module Space::Core
|
|
|
84
84
|
[entrypoint_content].pack("m0")
|
|
85
85
|
end
|
|
86
86
|
|
|
87
|
+
def local_space_architect?
|
|
88
|
+
space.path.join("repos/space-architect").directory?
|
|
89
|
+
end
|
|
90
|
+
|
|
87
91
|
def render_template(name)
|
|
88
92
|
template_path = TEMPLATE_DIR.join(name)
|
|
89
93
|
ERB.new(template_path.read, trim_mode: "-").result(binding)
|
|
@@ -7,12 +7,15 @@ module Space::Core
|
|
|
7
7
|
class OciRunner
|
|
8
8
|
include Dry::Monads[:result]
|
|
9
9
|
|
|
10
|
+
# Always-forwarded substrate auth (the in-image architect/claude reach for these).
|
|
11
|
+
# Payload-specific vars are declared per-space via `run.env:` or the --env flag.
|
|
10
12
|
AUTH_ENV = %w[ANTHROPIC_API_KEY CLAUDE_CODE_OAUTH_TOKEN ANTHROPIC_BASE_URL].freeze
|
|
11
13
|
|
|
12
|
-
def initialize(space:, env: ENV.to_h, interactive: true)
|
|
14
|
+
def initialize(space:, env: ENV.to_h, interactive: true, env_vars: [])
|
|
13
15
|
@space = space
|
|
14
16
|
@env = env
|
|
15
17
|
@interactive = interactive
|
|
18
|
+
@env_vars = (space.run_env + Array(env_vars)).map(&:to_s)
|
|
16
19
|
end
|
|
17
20
|
|
|
18
21
|
def image
|
|
@@ -36,7 +39,7 @@ module Space::Core
|
|
|
36
39
|
argv = [
|
|
37
40
|
"container", "run", "--rm",
|
|
38
41
|
*(@interactive ? ["-i", "-t"] : []),
|
|
39
|
-
*
|
|
42
|
+
*env_flags,
|
|
40
43
|
*mount_flags,
|
|
41
44
|
image,
|
|
42
45
|
*extra
|
|
@@ -44,17 +47,30 @@ module Space::Core
|
|
|
44
47
|
Success(argv)
|
|
45
48
|
end
|
|
46
49
|
|
|
50
|
+
# Explicitly-requested vars (run.env: + --env) that are unset/empty in the host
|
|
51
|
+
# env, so the caller can warn — silence here would let a missing key fail opaquely
|
|
52
|
+
# inside the guest. AUTH_ENV is opportunistic and intentionally excluded.
|
|
53
|
+
def missing_env
|
|
54
|
+
@env_vars.uniq.reject { |var| present?(var) }
|
|
55
|
+
end
|
|
56
|
+
|
|
47
57
|
private
|
|
48
58
|
|
|
49
59
|
attr_reader :space, :env, :interactive
|
|
50
60
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
61
|
+
# Bare `-e VAR` passthrough (no =value on the command line, so the secret never
|
|
62
|
+
# lands in argv/ps): substrate auth first, then the space's declared payload vars.
|
|
63
|
+
def env_flags
|
|
64
|
+
(AUTH_ENV + @env_vars).uniq.each_with_object([]) do |var, flags|
|
|
65
|
+
flags.push("-e", var) if present?(var)
|
|
55
66
|
end
|
|
56
67
|
end
|
|
57
68
|
|
|
69
|
+
def present?(var)
|
|
70
|
+
val = env[var]
|
|
71
|
+
val && !val.empty?
|
|
72
|
+
end
|
|
73
|
+
|
|
58
74
|
def mount_flags
|
|
59
75
|
mounts.each_with_object([]) do |(host, guest), flags|
|
|
60
76
|
flags.push("-v", "#{host}:#{guest}")
|
data/lib/space_core/space.rb
CHANGED
|
@@ -56,6 +56,12 @@ module Space::Core
|
|
|
56
56
|
Array(data.dig("pack", "persist")).map(&:to_s)
|
|
57
57
|
end
|
|
58
58
|
|
|
59
|
+
# Host env vars to forward into the container at `space run` (bare passthrough).
|
|
60
|
+
# Declared per-space so payloads needing credentials run without a wall of flags.
|
|
61
|
+
def run_env
|
|
62
|
+
Array(data.dig("run", "env")).map(&:to_s)
|
|
63
|
+
end
|
|
64
|
+
|
|
59
65
|
def architect
|
|
60
66
|
data["project"]
|
|
61
67
|
end
|
|
@@ -30,18 +30,6 @@ RUN apt-get update -qq \
|
|
|
30
30
|
# Claude Code CLI (https://claude.ai/install.sh)
|
|
31
31
|
RUN curl -fsSL https://claude.ai/install.sh | bash
|
|
32
32
|
|
|
33
|
-
# Copy the space tree into the image (filtered by Dockerfile.dockerignore alongside this Dockerfile)
|
|
34
|
-
COPY . /space
|
|
35
|
-
|
|
36
|
-
# Install space-architect gem: prefer in-space checkout for a pinned version, fall back to RubyGems
|
|
37
|
-
RUN if [ -d /space/repos/space-architect ]; then \
|
|
38
|
-
cd /space/repos/space-architect \
|
|
39
|
-
&& rake build \
|
|
40
|
-
&& gem install --no-document pkg/*.gem; \
|
|
41
|
-
else \
|
|
42
|
-
gem install --no-document space-architect; \
|
|
43
|
-
fi
|
|
44
|
-
|
|
45
33
|
# Write entrypoint: sets git safe.directory, execs requested command or a login shell
|
|
46
34
|
RUN echo '<%= entrypoint_b64 %>' | base64 -d > /entrypoint.sh \
|
|
47
35
|
&& chmod +x /entrypoint.sh
|
|
@@ -52,11 +40,33 @@ RUN printf 'export PATH="/usr/local/bundle/bin:/root/.local/bin:$PATH"\n' \
|
|
|
52
40
|
<% if provision_scripts.any? -%>
|
|
53
41
|
|
|
54
42
|
# Space-declared build-time provisioning (pack.provision)
|
|
43
|
+
# Each script is COPY'd individually — a space-content edit elsewhere does not bust its cache layer.
|
|
55
44
|
<% provision_scripts.each do |script| -%>
|
|
45
|
+
COPY <%= script %> /space/<%= script %>
|
|
56
46
|
RUN /space/<%= script %>
|
|
57
47
|
<% end -%>
|
|
58
48
|
<% end -%>
|
|
59
49
|
|
|
50
|
+
# Install space-architect gem
|
|
51
|
+
<% if local_space_architect? -%>
|
|
52
|
+
COPY repos/space-architect /space/repos/space-architect
|
|
53
|
+
RUN cd /space/repos/space-architect \
|
|
54
|
+
&& rake build \
|
|
55
|
+
&& gem install --no-document pkg/*.gem
|
|
56
|
+
<% else -%>
|
|
57
|
+
RUN gem install --no-document space-architect
|
|
58
|
+
<% end -%>
|
|
59
|
+
|
|
60
|
+
# Copy the space tree into the image (filtered by Dockerfile.dockerignore alongside this Dockerfile)
|
|
61
|
+
COPY . /space
|
|
62
|
+
<% if persist_paths.any? -%>
|
|
63
|
+
|
|
64
|
+
# Seed snapshot: capture baked content of persisted dirs so a first-run (empty) mount is seeded from it
|
|
65
|
+
<% persist_paths.each do |guest_path| -%>
|
|
66
|
+
RUN mkdir -p "/opt/space-seed<%= guest_path %>" && cp -a "<%= guest_path %>/." "/opt/space-seed<%= guest_path %>/" 2>/dev/null || true
|
|
67
|
+
<% end -%>
|
|
68
|
+
<% end -%>
|
|
69
|
+
|
|
60
70
|
WORKDIR /space
|
|
61
71
|
|
|
62
72
|
ENTRYPOINT ["/entrypoint.sh"]
|
|
@@ -3,6 +3,12 @@ set -e
|
|
|
3
3
|
git config --global --add safe.directory '*'
|
|
4
4
|
git config --global --get user.name >/dev/null 2>&1 || git config --global user.name 'space-architect'
|
|
5
5
|
git config --global --get user.email >/dev/null 2>&1 || git config --global user.email 'architect@localhost'
|
|
6
|
+
<% persist_paths.each do |guest_path| -%>
|
|
7
|
+
if [ -d "/opt/space-seed<%= guest_path %>" ] && [ -z "$(ls -A "<%= guest_path %>" 2>/dev/null)" ]; then
|
|
8
|
+
mkdir -p "<%= guest_path %>"
|
|
9
|
+
cp -a "/opt/space-seed<%= guest_path %>/." "<%= guest_path %>/" 2>/dev/null || true
|
|
10
|
+
fi
|
|
11
|
+
<% end -%>
|
|
6
12
|
if [ "$#" -eq 0 ]; then
|
|
7
13
|
exec bash --login
|
|
8
14
|
else
|
data/lib/space_core/version.rb
CHANGED