@north-light/cli 0.3.592 → 0.3.594

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": "@north-light/cli",
3
- "version": "0.3.592",
3
+ "version": "0.3.594",
4
4
  "description": "crtr — agent runtime with memory, plugins, and marketplaces",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -145,7 +145,7 @@
145
145
  },
146
146
  "license": "UNLICENSED",
147
147
  "dependencies": {
148
- "@north-light/identity": "0.3.592",
148
+ "@north-light/identity": "0.3.594",
149
149
  "@earendil-works/chord": "1.1.0",
150
150
  "@earendil-works/pi-agent-core": "1.1.0",
151
151
  "@earendil-works/pi-ai": "1.1.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@north-light/identity",
3
- "version": "0.3.592",
3
+ "version": "0.3.594",
4
4
  "description": "Northlight access-token verification and scope rules",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
package/runtime.lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@north-light/cli",
3
- "version": "0.3.592",
3
+ "version": "0.3.594",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@north-light/cli",
9
- "version": "0.3.592",
9
+ "version": "0.3.594",
10
10
  "hasInstallScript": true,
11
11
  "license": "UNLICENSED",
12
12
  "workspaces": [
@@ -28,7 +28,7 @@
28
28
  "@milkdown/preset-gfm": "^7.22.3",
29
29
  "@milkdown/prose": "^7.22.3",
30
30
  "@milkdown/utils": "^7.22.3",
31
- "@north-light/identity": "0.3.592",
31
+ "@north-light/identity": "0.3.594",
32
32
  "cron-parser": "^5.6.0",
33
33
  "esbuild": "^0.27.7",
34
34
  "jose": "^6.2.12",
@@ -11956,17 +11956,17 @@
11956
11956
  },
11957
11957
  "packages/crouter-api": {
11958
11958
  "name": "@north-light/api",
11959
- "version": "0.3.592",
11959
+ "version": "0.3.594",
11960
11960
  "license": "UNLICENSED"
11961
11961
  },
11962
11962
  "packages/crouter-env-docker": {
11963
11963
  "name": "@north-light/env-docker",
11964
- "version": "0.3.592",
11964
+ "version": "0.3.594",
11965
11965
  "license": "UNLICENSED"
11966
11966
  },
11967
11967
  "packages/crouter-identity": {
11968
11968
  "name": "@north-light/identity",
11969
- "version": "0.3.592",
11969
+ "version": "0.3.594",
11970
11970
  "license": "UNLICENSED",
11971
11971
  "dependencies": {
11972
11972
  "jose": "^6.2.1"
@@ -11974,21 +11974,21 @@
11974
11974
  },
11975
11975
  "packages/crouter-plugin": {
11976
11976
  "name": "@north-light/plugin",
11977
- "version": "0.3.592",
11977
+ "version": "0.3.594",
11978
11978
  "license": "UNLICENSED",
11979
11979
  "dependencies": {
11980
- "@north-light/api": "0.3.592",
11981
- "@north-light/identity": "0.3.592"
11980
+ "@north-light/api": "0.3.594",
11981
+ "@north-light/identity": "0.3.594"
11982
11982
  }
11983
11983
  },
11984
11984
  "packages/crouter-sdk": {
11985
11985
  "name": "@north-light/sdk",
11986
- "version": "0.3.592",
11986
+ "version": "0.3.594",
11987
11987
  "license": "UNLICENSED",
11988
11988
  "dependencies": {
11989
- "@north-light/api": "0.3.592",
11990
- "@north-light/identity": "0.3.592",
11991
- "@north-light/plugin": "0.3.592",
11989
+ "@north-light/api": "0.3.594",
11990
+ "@north-light/identity": "0.3.594",
11991
+ "@north-light/plugin": "0.3.594",
11992
11992
  "jose": "^6.2.1"
11993
11993
  },
11994
11994
  "bin": {
@@ -5,8 +5,11 @@
5
5
  # CRTR_RELEASE_URL, CRTR_RELEASE_SHA256 and the rest the runner set. Once per sandbox it creates the `crtr` user
6
6
  # (`crtrd` refuses root) and its directories; it installs the release if it is absent (download, sha256 check,
7
7
  # unpack into a temp dir, rename into place, keep three), then hands over to `crtr-sandbox-start` as `crtr`.
8
- # The node token reaches the start script on stdin, never in its environment, so no process of user `crtr` can
9
- # read it from /proc. A failed download or checksum exits non-zero and prints `crtr-sandbox-start: failed <reason>`
8
+ # The node token reaches the start script on stdin, never in its environment, so it is in no process's
9
+ # /proc/<pid>/environ. It is not hidden from user `crtr` otherwise: the broker holds it in memory, and the agent's
10
+ # shell runs as the same user (design-fleet D13/D78 scope the token to this execution's node, nothing more). The final `exec` chain forks nothing (busybox `nsenter -S/-G` execs, and the token goes in as a here-document, not a pipe, which would
11
+ # put a forking shell in front of it), so the process Blaxel tracks as `broker-<execution id>` is the start script itself: stopping it reaches
12
+ # the node and the broker, and its exit means the broker is gone. A failed download or checksum exits non-zero and prints `crtr-sandbox-start: failed <reason>`
10
13
  # as its last line (the runner's `status` reports that reason).
11
14
  set -eu
12
15
 
@@ -51,5 +54,8 @@ done
51
54
 
52
55
  token=$CRTR_NODE_TOKEN
53
56
  unset CRTR_NODE_TOKEN
54
- # `su -p` keeps the rest of the environment; the start script derives HOME and the layout itself.
55
- printf '%s' "$token" | exec su -p -s /bin/sh "$RUN_USER" -c "exec node '$RELEASE_DIR/scripts/sandbox-start.mjs'"
57
+ # busybox `su` forks, which leaves the tracked process a wrapper whose children outlive a stop; `nsenter` with no namespace flag only sets the
58
+ # uid and gid, then execs. It keeps the environment (as `su -p` did); the start script derives HOME and the layout itself.
59
+ exec nsenter -S "$(id -u "$RUN_USER")" -G "$(id -g "$RUN_USER")" node "$RELEASE_DIR/scripts/sandbox-start.mjs" <<EOT
60
+ $token
61
+ EOT
@@ -344,34 +344,71 @@ async function start(env) {
344
344
  gitConfig('credential.https://github.com.helper', join(releaseDir, 'scripts', 'git-credential-crtr'));
345
345
  if (bundle.git_identity) { gitConfig('user.name', bundle.git_identity.name); gitConfig('user.email', bundle.git_identity.email); }
346
346
 
347
- // 5.5 plugins, once per revision.
347
+ // 5.5 plugins, once per revision. The person's install rows are already authoritative on the control plane, and no broker or daemon exists yet, so
348
+ // this materializes each plugin's code the way `pkg plugin install` lays it out in the user scope (`<runtime>/marketplaces/<name>`, `<runtime>/plugins/<plugin>`)
349
+ // with git alone; the release CLI would load the person's settings from a daemon this start cannot reach.
348
350
  const marker = join(work, '.crtr', 'plugins-revision');
349
351
  const have = existsSync(marker) ? readFileSync(marker, 'utf8').trim() : null;
350
- if (have !== String(bundle.plugins_revision) && bundle.plugins.length > 0) {
351
- const crtr = join(releaseDir, 'bin', 'crtr');
352
- const pluginEnv = { ...gitEnv, NPM_CONFIG_PREFIX: join(home, '.npm-global'), CRTR_HOME: root, CRTR_NO_AUTO_INIT: '1' };
353
- const crtrRun = (args) => run(process.execPath, [crtr, ...args], { env: pluginEnv, timeout: 300_000 });
352
+ if (have !== String(bundle.plugins_revision)) {
353
+ const pluginsDir = join(root, 'runtime', 'plugins');
354
+ const marketplacesDir = join(root, 'runtime', 'marketplaces');
355
+ const cloneEnv = { ...gitEnv, GIT_TERMINAL_PROMPT: '0' };
354
356
  const detail = (r) => (r.stderr || r.stdout || '').slice(0, 300);
355
- // A plugin of a marketplace is installed as `<marketplace name>/<plugin>`; the bundle names only the marketplace's url and ref, so the name is what
356
- // `pkg market add` reports. Adding re-clones, so a marketplace added once per start is current. A plugin already installed (an earlier revision) is
357
- // removed first, since install refuses an installed name.
357
+ const cloneInto = (url, ref, dest) => {
358
+ const r = run('git', ['clone', '--depth', '1', ...(ref ? ['--branch', ref] : []), url, dest], { env: cloneEnv, timeout: 300_000 });
359
+ return r.status === 0 ? null : detail(r) || `git clone exited ${r.status}`;
360
+ };
361
+ // The directory holds exactly the bundle's plugins: one an earlier revision installed and this one no longer lists is removed.
362
+ let current = null;
363
+ try {
364
+ const wanted = new Set(bundle.plugins.map((plugin) => plugin.name));
365
+ if (existsSync(pluginsDir)) for (const name of readdirSync(pluginsDir)) if (!wanted.has(name)) rmSync(join(pluginsDir, name), { recursive: true, force: true });
366
+ const readJson = (path) => { try { return JSON.parse(readFileSync(path, 'utf8')); } catch { return null; } };
367
+ // A plugin of a marketplace is installed as `<plugin>` from the marketplace the bundle's url and ref name. Cloning again each start keeps a marketplace
368
+ // current; a plugin installed by an earlier revision is replaced.
358
369
  const marketplaces = new Map();
359
370
  for (const plugin of bundle.plugins) {
371
+ current = plugin.name;
360
372
  if ('path' in plugin.source) throw new StartFailure('plugin_source_unreachable', `${plugin.name}: its source is ${plugin.source.path} on the machine that registered it, which a sandbox cannot read`);
361
373
  const { url, ref } = plugin.source.marketplace;
362
374
  if (!url) throw new StartFailure('plugin_source_unreachable', `${plugin.name}: its marketplace has no url`);
375
+ const fail = (message) => new StartFailure('plugin_install_failed', `${plugin.name}: ${message}`);
376
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(plugin.name)) throw fail('invalid plugin name');
363
377
  const key = `${url}\0${ref ?? ''}`;
364
378
  if (!marketplaces.has(key)) {
365
- const added = crtrRun(['--json', 'pkg', 'market', 'add', url, ...(ref ? ['--ref', ref] : []), '--scope', 'user']);
366
- let name;
367
- try { name = JSON.parse(added.stdout).name; } catch { /* reported below */ }
368
- if (added.status !== 0 || typeof name !== 'string') throw new StartFailure('plugin_install_failed', `${plugin.name}: marketplace ${url}: ${detail(added)}`);
369
- marketplaces.set(key, name);
379
+ mkdirSync(marketplacesDir, { recursive: true });
380
+ const staging = join(marketplacesDir, `.clone-${process.pid}-${marketplaces.size}`);
381
+ rmSync(staging, { recursive: true, force: true });
382
+ const cloneError = cloneInto(url, ref, staging);
383
+ if (cloneError !== null) throw fail(`marketplace ${url}: ${cloneError}`);
384
+ const manifest = readJson(join(staging, '.crouter-marketplace', 'marketplace.json'));
385
+ if (typeof manifest?.name !== 'string' || !Array.isArray(manifest.plugins)) { rmSync(staging, { recursive: true, force: true }); throw fail(`marketplace ${url}: not a valid marketplace`); }
386
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(manifest.name)) { rmSync(staging, { recursive: true, force: true }); throw fail(`marketplace ${url}: invalid marketplace name ${JSON.stringify(manifest.name)}`); }
387
+ const dest = join(marketplacesDir, manifest.name);
388
+ rmSync(dest, { recursive: true, force: true });
389
+ renameSync(staging, dest);
390
+ marketplaces.set(key, { dest, manifest });
370
391
  }
371
- const removed = crtrRun(['pkg', 'plugin', 'remove', plugin.name, '--scope', 'user']);
372
- if (removed.status !== 0 && !/not found/i.test(removed.stderr + removed.stdout)) throw new StartFailure('plugin_install_failed', `${plugin.name}: remove: ${detail(removed)}`);
373
- const installed = crtrRun(['pkg', 'plugin', 'install', `${marketplaces.get(key)}/${plugin.name}`, '--scope', 'user']);
374
- if (installed.status !== 0) throw new StartFailure('plugin_install_failed', `${plugin.name}: ${detail(installed)}`);
392
+ const { dest: marketplaceDir, manifest } = marketplaces.get(key);
393
+ const entry = manifest.plugins.find((candidate) => candidate?.name === plugin.name);
394
+ if (!entry || typeof entry.source !== 'string') throw fail(`not found in marketplace ${manifest.name}`);
395
+ const target = join(pluginsDir, plugin.name);
396
+ mkdirSync(pluginsDir, { recursive: true });
397
+ rmSync(target, { recursive: true, force: true });
398
+ if (/^(https?:\/\/|git@|ssh:\/\/|file:\/\/)/.test(entry.source) || entry.source.endsWith('.git')) {
399
+ const cloneError = cloneInto(entry.source, undefined, target);
400
+ if (cloneError !== null) throw fail(cloneError);
401
+ } else {
402
+ const source = resolve(marketplaceDir, entry.source);
403
+ if (!existsSync(source) || !within(realpathSync(marketplaceDir), realpathSync(source))) throw fail(`plugin source ${entry.source} is not in marketplace ${manifest.name}`);
404
+ symlinkSync(source, target, 'dir');
405
+ }
406
+ if (readJson(join(target, '.crouter-plugin', 'plugin.json')) === null) { rmSync(target, { recursive: true, force: true }); throw fail('its .crouter-plugin/plugin.json is missing or invalid'); }
407
+ }
408
+ } catch (error) {
409
+ // A filesystem error (mkdir, remove, rename, symlink) is an install failure of the plugin being installed, not a generic start failure.
410
+ if (error instanceof StartFailure) throw error;
411
+ throw new StartFailure('plugin_install_failed', `${current ?? 'plugins'}: ${error.message}`.slice(0, 300));
375
412
  }
376
413
  }
377
414
  if (have !== String(bundle.plugins_revision)) { mkdirSync(dirname(marker), { recursive: true }); writeFileSync(marker, String(bundle.plugins_revision)); }