yolocage 0.1.0 → 0.1.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.
package/lib/config.js CHANGED
@@ -54,8 +54,13 @@ function normalize(raw) {
54
54
  ? path.resolve(raw.config_dir)
55
55
  : td.configDirHost;
56
56
 
57
- // bind_dirs (replace): if the user set it, use ONLY those + the
58
- // workspace + the config_dir. If unset, generate the type defaults.
57
+ // bind_dirs (replace): if the user set it, use ONLY those. If unset,
58
+ // generate the type defaults — workspace, config dir, and any
59
+ // type-declared single-file mounts (e.g. claude's ~/.claude.json).
60
+ // The bind_dirs=… semantic is documented as a full replace, so the
61
+ // type-default file mounts get dropped along with the dir defaults
62
+ // if the user opts in to bind_dirs. extra_bind_dirs= remains the
63
+ // append-style escape hatch for adding mounts back on top.
59
64
  let bindDirs;
60
65
  if (Array.isArray(raw.bind_dirs) && raw.bind_dirs.length > 0) {
61
66
  bindDirs = raw.bind_dirs.map(parseBindSpec);
@@ -64,6 +69,17 @@ function normalize(raw) {
64
69
  { host: workspace, container: '/workspace', mode: 'rw' },
65
70
  { host: configDirHost, container: td.configDirContainer, mode: 'rw' },
66
71
  ];
72
+ // Type-declared single-file binds. Tagged `kind: 'file'` so
73
+ // lib/docker.js can touch them into existence at exec time —
74
+ // docker auto-creates an empty *directory* on the host when the
75
+ // path is missing (long-standing footgun), which breaks the
76
+ // mount on the container side. Side-effect kept out of this
77
+ // pure-data layer so tests don't write to the operator's $HOME.
78
+ for (const f of td.extraHostFiles || []) {
79
+ bindDirs.push({
80
+ host: f.host, container: f.container, mode: 'rw', kind: 'file',
81
+ });
82
+ }
67
83
  }
68
84
 
69
85
  const extraBindDirs = (raw.extra_bind_dirs || []).map(parseBindSpec);
package/lib/docker.js CHANGED
@@ -10,8 +10,30 @@
10
10
 
11
11
  'use strict';
12
12
 
13
+ const fs = require('fs');
14
+ const path = require('path');
13
15
  const { spawnSync, execFileSync } = require('child_process');
14
16
 
17
+ // Touch any bind specs tagged `kind: 'file'` into existence on the host.
18
+ // `docker run -v <host>:<container>` auto-creates an empty DIRECTORY
19
+ // at the host path when it's missing (long-standing footgun); single-
20
+ // file binds (e.g. claude's ~/.claude.json) need an actual file there
21
+ // or the mount goes wrong. No-op for paths that already exist; no-op
22
+ // for entries without the `kind: 'file'` tag. Best-effort: an EACCES
23
+ // or similar isn't fatal — docker will surface its own error later.
24
+ function materializeFileBinds(spec) {
25
+ for (const b of spec.bindDirs || []) {
26
+ if (b.kind !== 'file') continue;
27
+ if (fs.existsSync(b.host)) continue;
28
+ try {
29
+ fs.mkdirSync(path.dirname(b.host), { recursive: true });
30
+ fs.writeFileSync(b.host, '', { mode: 0o600 });
31
+ } catch (_e) {
32
+ // swallow — surface as docker's own error if it matters
33
+ }
34
+ }
35
+ }
36
+
15
37
  let _sudoChecked = false;
16
38
  let _useSudo = false;
17
39
  let _noticePrinted = false;
@@ -75,6 +97,7 @@ function _printNoticeOnce(stderr) {
75
97
  // { argv: [...], cmd: [...] } for piping into spawnSync(argv[0], argv.slice(1).concat(cmd))
76
98
  function buildRunArgs(spec, opts) {
77
99
  opts = opts || {};
100
+ materializeFileBinds(spec);
78
101
  const args = ['run'];
79
102
  if (opts.rm !== false) args.push('--rm');
80
103
  if (opts.interactive !== false) args.push('-it');
package/lib/types.js CHANGED
@@ -1,7 +1,5 @@
1
1
  // Type defaults table. The shortcut form (`yc claude`) and named-cage
2
- // `--type=…` flag pick from these. v0 ships claude + codex enabled; opencode
3
- // is sketched in but `v0:false` so the CLI errors cleanly if the operator
4
- // asks for it.
2
+ // `--type=…` flag pick from these. v0 ships claude + codex.
5
3
  //
6
4
  // Each entry's `bindDirs` describes the default mounts for a fresh cage:
7
5
  // workspace: a host path mounted at /workspace (default = cwd)
@@ -10,6 +8,11 @@
10
8
  //
11
9
  // `cmd` is the in-container command line the entrypoint exec()s by default.
12
10
  // CLI pass-through args (`yc claude -- --resume`) are appended after.
11
+ //
12
+ // Images are published from .github/workflows/publish-images.yml to
13
+ // ghcr.io/jlamendo/yolocage-<name> as multi-arch (linux/amd64 +
14
+ // linux/arm64) manifests. `:latest` follows main; pinned tags are
15
+ // optionally emitted via the workflow_dispatch input.
13
16
 
14
17
  'use strict';
15
18
 
@@ -21,9 +24,21 @@ const HOME = os.homedir();
21
24
  const TYPE_DEFAULTS = {
22
25
  claude: {
23
26
  v0: true,
24
- image: 'yolocage/claude:dev',
27
+ image: 'ghcr.io/jlamendo/yolocage-claude:latest',
25
28
  configDirHost: path.join(HOME, '.claude'),
26
29
  configDirContainer: '/home/agent/.claude',
30
+ // claude-code reads config from BOTH `~/.claude/` (directory, project
31
+ // sessions + backups) AND `~/.claude.json` (file, anchored auth +
32
+ // global config). The dir is the `configDir*` mount above; the
33
+ // file lives at the same level outside the dir, so it needs its
34
+ // own bind. Without this, the container starts with no auth and
35
+ // the CLI prompts the operator to log in fresh every cage.
36
+ // (`extraHostFiles` is touched into existence at normalize time if
37
+ // missing on the host, so a never-logged-in operator gets an empty
38
+ // host file that claude-code populates on first sign-in.)
39
+ extraHostFiles: [
40
+ { host: path.join(HOME, '.claude.json'), container: '/home/agent/.claude.json' },
41
+ ],
27
42
  // --continue auto-resumes the most recent in-cwd conversation; harmless on
28
43
  // first run (claude falls back to a fresh session). With per-cwd persistent
29
44
  // cages, this is what the operator wants by default — `yc claude` always
@@ -32,18 +47,11 @@ const TYPE_DEFAULTS = {
32
47
  },
33
48
  codex: {
34
49
  v0: true,
35
- image: 'yolocage/codex:dev',
50
+ image: 'ghcr.io/jlamendo/yolocage-codex:latest',
36
51
  configDirHost: path.join(HOME, '.codex'),
37
52
  configDirContainer: '/home/agent/.codex',
38
53
  cmd: ['codex', '--full-auto'],
39
54
  },
40
- opencode: {
41
- v0: false,
42
- image: 'yolocage/opencode:dev',
43
- configDirHost: path.join(HOME, '.config', 'opencode'),
44
- configDirContainer: '/home/agent/.config/opencode',
45
- cmd: ['opencode'],
46
- },
47
55
  };
48
56
 
49
57
  function isKnownType(type) {
package/lib/update.js CHANGED
@@ -5,8 +5,10 @@
5
5
  // We probe how the user is invoking yc (global npm install vs npx
6
6
  // run vs unknown) and either install/upgrade or print a helpful
7
7
  // no-op message.
8
- // 2. Cage images: re-pull yolocage/claude:latest + yolocage/codex:latest
9
- // so the next cage launch starts from current bytes.
8
+ // 2. Cage images: re-pull each ghcr.io/jlamendo/yolocage-* image so
9
+ // the next cage launch starts from current bytes. The exact refs
10
+ // come from TYPE_DEFAULTS — comment is descriptive, not
11
+ // authoritative.
10
12
  //
11
13
  // Sudo handling mirrors lib/docker.js — npm global installs typically
12
14
  // land in a path that requires root (/usr/local/lib/node_modules on
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yolocage",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Sandboxed claude-code / codex container with built-in egress credential scrubber. Run AI coding agents in --dangerously-skip-permissions mode with a safety net.",
5
5
  "license": "MIT",
6
6
  "author": "yolocage contributors",