@arthovlabs/liber 0.5.0 → 0.5.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arthovlabs/liber",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "description": "LiBER — autonomous agent orchestrator with a machine-enforced grounding gate and a skill library",
5
5
  "bin": {
6
6
  "liber": "bin/liber",
@@ -268,10 +268,15 @@ function auditTerminal(entry) {
268
268
  function containerPolicy() {
269
269
  const term = policy.terminalConfig();
270
270
  const requested = term.backend === 'docker';
271
+ const image = term.image || 'alpine:latest';
272
+ const usable = requested ? containerUsable(false, image) : { ok: false, error: null };
271
273
  return {
272
- on: requested && containerAvailable(),
274
+ // A daemon that answers but cannot start a container would make every
275
+ // command fail; treat "usable" as the condition, and say why otherwise.
276
+ on: requested && usable.ok,
273
277
  requested,
274
- image: term.image || 'alpine:latest',
278
+ fallback_reason: requested && !usable.ok ? (usable.error || 'container runtime not usable') : null,
279
+ image,
275
280
  network: term.allow_network === true,
276
281
  memory: term.memory || '512m',
277
282
  pids: Number(term.pids) || 128
@@ -292,12 +297,45 @@ function containerAvailable(force = false) {
292
297
  return ok;
293
298
  }
294
299
 
300
+ let _runCache = null;
301
+ /**
302
+ * Can this machine actually START a container with the configured image?
303
+ * A reachable daemon is not the same thing: a CI runner can have the CLI and a
304
+ * server version and still refuse to run (no image, no permissions). Checking by
305
+ * running is the only honest answer, and it is cached because it costs ~1s.
306
+ */
307
+ function containerUsable(force = false, image = 'alpine:latest') {
308
+ if (!force && _runCache && _runCache.image === image && Date.now() - _runCache.at < 300_000) return _runCache;
309
+ if (!containerAvailable(force)) {
310
+ _runCache = { ok: false, image, at: Date.now(), error: 'docker daemon not reachable' };
311
+ return _runCache;
312
+ }
313
+ let ok = false;
314
+ let error = null;
315
+ try {
316
+ require('child_process').execFileSync('docker', ['run', '--rm', '--network', 'none', image, 'true'],
317
+ { timeout: 60000, windowsHide: true, stdio: ['ignore', 'ignore', 'pipe'] });
318
+ ok = true;
319
+ } catch (e) {
320
+ error = String(e.stderr || e.message || 'container run failed').slice(0, 200);
321
+ }
322
+ _runCache = { ok, image, at: Date.now(), error };
323
+ return _runCache;
324
+ }
325
+
295
326
  function runContainer(command, { cwd, timeout, profile }) {
296
327
  const { execFile } = require('child_process');
297
328
  const mount = `${path.resolve(process.cwd())}:/work:rw`;
329
+ // On POSIX hosts run as the invoking user: files the container writes land
330
+ // owned by them instead of root, which is both safer and the only way a write
331
+ // into the mount works under a rootless daemon.
332
+ const asUser = process.platform !== 'win32' && typeof process.getuid === 'function'
333
+ ? ['--user', `${process.getuid()}:${process.getgid()}`]
334
+ : [];
298
335
  const args = [
299
336
  'run', '--rm', '--interactive',
300
337
  ...(profile.network ? ['--network', 'bridge'] : ['--network', 'none']),
338
+ ...asUser,
301
339
  '--read-only',
302
340
  '--tmpfs', '/tmp:rw,noexec,nosuid,size=64m',
303
341
  '--memory', profile.memory,
@@ -445,5 +483,6 @@ module.exports = {
445
483
  sandboxPolicy,
446
484
  sandboxEnv,
447
485
  containerPolicy,
448
- containerAvailable
486
+ containerAvailable,
487
+ containerUsable
449
488
  };
@@ -26,8 +26,11 @@ function isGitRepo() {
26
26
  }
27
27
 
28
28
  function branch() {
29
+ // `rev-parse --abbrev-ref HEAD` prints the literal string "HEAD" on a detached
30
+ // checkout (CI tag builds do this), which is not a branch name — reporting it
31
+ // as one made the UI show a branch called HEAD and marked nothing current.
29
32
  const b = sh('git', ['rev-parse', '--abbrev-ref', 'HEAD']);
30
- if (b && b.trim()) return b.trim();
33
+ if (b && b.trim() && b.trim() !== 'HEAD') return b.trim();
31
34
  const detached = sh('git', ['rev-parse', '--short', 'HEAD']);
32
35
  return detached ? `detached@${detached.trim()}` : null;
33
36
  }
@@ -158,6 +161,9 @@ function gitState() {
158
161
  is_git: true,
159
162
  root: git(['rev-parse', '--show-toplevel']).out || process.cwd(),
160
163
  branch: current,
164
+ // CI checks out a commit, not a branch: say so instead of pretending a
165
+ // branch is current and letting the UI mark nothing.
166
+ detached: String(current || '').startsWith('detached@'),
161
167
  branches: list.map(name => ({ name, current: name === current })),
162
168
  remote: remoteUrl ? { name: 'origin', url: remoteUrl } : null,
163
169
  ahead, behind, dirty