@camstack/agent 1.0.8 → 1.1.1

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/dist/cli.js CHANGED
@@ -24,7 +24,10 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
24
24
  ));
25
25
 
26
26
  // src/cli.ts
27
+ var import_node_fs = require("fs");
28
+ var import_node_module = require("module");
27
29
  var os3 = __toESM(require("os"));
30
+ var path7 = __toESM(require("path"));
28
31
 
29
32
  // src/agent-bootstrap.ts
30
33
  var fs6 = __toESM(require("fs"));
@@ -369,6 +372,37 @@ async function applyDeployedBundle(input) {
369
372
  return { addonDir: liveDir };
370
373
  }
371
374
 
375
+ // src/fetch-bundle-from-hub.ts
376
+ var import_node_crypto2 = require("crypto");
377
+ var import_undici = require("undici");
378
+ async function fetchBundleFromHub(opts) {
379
+ const authHeader = { Authorization: `Bearer ${opts.token}`, "accept-encoding": "identity" };
380
+ const res = opts.fetchImpl ? await opts.fetchImpl(opts.url, { headers: authHeader }) : await (0, import_undici.fetch)(opts.url, {
381
+ headers: authHeader,
382
+ dispatcher: new import_undici.Agent({ connect: { rejectUnauthorized: false } })
383
+ });
384
+ if (!res.ok) {
385
+ throw new Error(`bundle fetch failed: HTTP ${res.status}`);
386
+ }
387
+ const buffer = Buffer.from(await res.arrayBuffer());
388
+ if (buffer.length !== opts.bytes) {
389
+ throw new Error(`bundle bytes mismatch: expected ${opts.bytes}, got ${buffer.length}`);
390
+ }
391
+ const sha = (0, import_node_crypto2.createHash)("sha256").update(buffer).digest("hex");
392
+ if (sha !== opts.sha256) {
393
+ throw new Error(`bundle sha256 mismatch: expected ${opts.sha256}, got ${sha}`);
394
+ }
395
+ return buffer;
396
+ }
397
+
398
+ // src/apply-model-distribution.ts
399
+ async function applyModelDistribution(seam, params) {
400
+ seam.mkdirp(seam.modelsDir);
401
+ const buffer = await seam.fetchBundle(params.source);
402
+ await seam.extract(buffer, seam.modelsDir);
403
+ return { success: true, modelId: params.modelId, format: params.format, path: seam.modelsDir };
404
+ }
405
+
372
406
  // src/agent-service.ts
373
407
  var deploySwapLogger = {
374
408
  info: (msg) => console.log(`[Agent] ${msg}`),
@@ -392,8 +426,8 @@ function getLocalIps() {
392
426
  }
393
427
  async function withTimeout(p, ms, fallback) {
394
428
  let timer;
395
- const timeout = new Promise((resolve5) => {
396
- timer = setTimeout(() => resolve5(fallback), ms);
429
+ const timeout = new Promise((resolve6) => {
430
+ timer = setTimeout(() => resolve6(fallback), ms);
397
431
  });
398
432
  try {
399
433
  return await Promise.race([p, timeout]);
@@ -402,6 +436,28 @@ async function withTimeout(p, ms, fallback) {
402
436
  }
403
437
  }
404
438
  var METRICS_SNAPSHOT_TIMEOUT_MS = 2e3;
439
+ function resolveDeployAction(seam) {
440
+ return async (params) => {
441
+ const { addonId, source, bundle } = params;
442
+ if (source && (0, import_system.isAddonDeploySource)(source)) {
443
+ if (source.kind === "npm") {
444
+ await seam.installFromNpm(addonId, source.version);
445
+ return { success: true, addonId };
446
+ }
447
+ const buf2 = await seam.fetchBundle(source);
448
+ const { addonDir: addonDir2 } = await seam.applyBundle(buf2);
449
+ seam.onApplied(addonDir2);
450
+ return { success: true, addonId, path: addonDir2 };
451
+ }
452
+ if (bundle === void 0) {
453
+ throw new Error("$agent.deploy: no source descriptor and no legacy bundle provided");
454
+ }
455
+ const buf = typeof bundle === "string" ? Buffer.from(bundle, "base64") : bundle;
456
+ const { addonDir } = await seam.applyBundle(buf);
457
+ seam.onApplied(addonDir);
458
+ return { success: true, addonId, path: addonDir };
459
+ };
460
+ }
405
461
  function readHubAddressFromConfig(configPath) {
406
462
  if (!configPath) return null;
407
463
  try {
@@ -574,8 +630,7 @@ function createAgentService(deps) {
574
630
  deploy: {
575
631
  handler: async (ctx) => {
576
632
  const { params } = ctx;
577
- const { addonId, bundle } = params;
578
- const bufferData = typeof bundle === "string" ? Buffer.from(bundle, "base64") : bundle;
633
+ const { addonId, source, bundle } = params;
579
634
  const { execFile } = await import("child_process");
580
635
  const { promisify } = await import("util");
581
636
  const execFileAsync = promisify(execFile);
@@ -593,17 +648,74 @@ function createAgentService(deps) {
593
648
  }
594
649
  }
595
650
  };
596
- const { addonDir } = await applyDeployedBundle({
597
- addonsDir: deps.addonsDir,
598
- addonId,
599
- bundle: bufferData,
600
- extract,
601
- logger: deploySwapLogger
602
- });
603
- for (const declId of readDeployedAddonIds(addonDir)) {
604
- deps.loadedAddons.delete(declId);
605
- }
606
- return { success: true, addonId, path: addonDir };
651
+ const seam = {
652
+ installFromNpm: (pkg, v) => {
653
+ if (!deps.installFromNpm) {
654
+ throw new Error("npm deploy source unsupported on this agent");
655
+ }
656
+ return deps.installFromNpm(pkg, v);
657
+ },
658
+ applyBundle: (buf) => applyDeployedBundle({
659
+ addonsDir: deps.addonsDir,
660
+ addonId,
661
+ bundle: buf,
662
+ extract,
663
+ logger: deploySwapLogger
664
+ }),
665
+ fetchBundle: (s) => fetchBundleFromHub(s),
666
+ // Evict every addon DECLARATION id this package contributes
667
+ // from `loadedAddons` so the follow-up `$agent.reload` actually
668
+ // re-instantiates it. `loadDeployedAddons` skips any addon
669
+ // still present in `loadedAddons`; without this eviction a
670
+ // redeploy would leave the agent pinned to the pre-update
671
+ // version. The `deploy` param `addonId` is the PACKAGE name
672
+ // (used only as the on-disk dir), whereas `loadedAddons` is
673
+ // keyed by the addon DECLARATION id — they differ for scoped
674
+ // packages, so we read the extracted manifest to bridge them.
675
+ onApplied: (addonDir) => {
676
+ for (const declId of readDeployedAddonIds(addonDir)) {
677
+ deps.loadedAddons.delete(declId);
678
+ }
679
+ }
680
+ };
681
+ return resolveDeployAction(seam)({ addonId, source, bundle });
682
+ }
683
+ },
684
+ /**
685
+ * Pull a staged model tarball from the hub and untar it into this node's
686
+ * `<dataDir>/models` (Model Studio P2). Reuses the agent-pull machinery:
687
+ * `fetchBundleFromHub` verifies bytes + sha256 before extraction. The
688
+ * existing `isModelDownloaded` then reports the model present, so the
689
+ * detection-pipeline can load it locally.
690
+ */
691
+ distributeModel: {
692
+ handler: async (ctx) => {
693
+ const { params } = ctx;
694
+ const { execFile } = await import("child_process");
695
+ const { promisify } = await import("util");
696
+ const execFileAsync = promisify(execFile);
697
+ const modelsDir = path4.join(deps.dataDir, "models");
698
+ const seam = {
699
+ modelsDir,
700
+ fetchBundle: (s) => fetchBundleFromHub(s),
701
+ extract: async (tgz, destDir) => {
702
+ const tmp = path4.join(
703
+ destDir,
704
+ `.dist-${Date.now()}-${Math.random().toString(36).slice(2)}.tgz`
705
+ );
706
+ fs4.writeFileSync(tmp, tgz);
707
+ try {
708
+ await execFileAsync("tar", ["-xzf", tmp, "-C", destDir], { timeout: 6e4 });
709
+ } finally {
710
+ try {
711
+ fs4.unlinkSync(tmp);
712
+ } catch {
713
+ }
714
+ }
715
+ },
716
+ mkdirp: (dir) => fs4.mkdirSync(dir, { recursive: true })
717
+ };
718
+ return applyModelDistribution(seam, params);
607
719
  }
608
720
  },
609
721
  /**
@@ -1070,7 +1182,10 @@ async function startAgent(configPath) {
1070
1182
  subtree.registerNode(params);
1071
1183
  triggerUpwardRegistration();
1072
1184
  },
1073
- expectedClusterSecretHash: config.secret ? (0, import_system3.hashClusterSecret)(config.secret) : void 0
1185
+ expectedClusterSecretHash: config.secret ? (0, import_system3.hashClusterSecret)(config.secret) : void 0,
1186
+ installFromNpm: async (pkg, version) => {
1187
+ await installer.install(pkg, version);
1188
+ }
1074
1189
  });
1075
1190
  broker.createService(agentServiceSchema);
1076
1191
  const agentTcpPort = (0, import_system3.deriveAgentListenPort)(broker.nodeID);
@@ -1649,6 +1764,28 @@ if (args.secret && !process.env["CAMSTACK_CLUSTER_SECRET"]) {
1649
1764
  if (args.statusPort && !process.env["CAMSTACK_STATUS_PORT"]) {
1650
1765
  process.env["CAMSTACK_STATUS_PORT"] = args.statusPort;
1651
1766
  }
1767
+ if (!process.env["CAMSTACK_FRAMEWORK_DIR"]) {
1768
+ const scriptPath = process.argv[1];
1769
+ if (scriptPath) {
1770
+ const frameworkDir = path7.resolve(path7.dirname(scriptPath), "..", "..");
1771
+ const frameworkModules = path7.join(frameworkDir, "node_modules");
1772
+ if ((0, import_node_fs.existsSync)(path7.join(frameworkModules, "@camstack", "shm-ring"))) {
1773
+ process.env["CAMSTACK_FRAMEWORK_DIR"] = frameworkDir;
1774
+ const existingNodePath = process.env["NODE_PATH"];
1775
+ process.env["NODE_PATH"] = existingNodePath ? `${frameworkModules}${path7.delimiter}${existingNodePath}` : frameworkModules;
1776
+ import_node_module.Module._initPaths();
1777
+ console.log(
1778
+ `[Agent] Self-contained framework detected \u2014 CAMSTACK_FRAMEWORK_DIR=${frameworkDir}`
1779
+ );
1780
+ if (!process.env["CAMSTACK_BUNDLED_ADDONS_DIR"]) {
1781
+ const bundledAddons = path7.join(frameworkDir, "addons");
1782
+ if ((0, import_node_fs.existsSync)(bundledAddons)) {
1783
+ process.env["CAMSTACK_BUNDLED_ADDONS_DIR"] = bundledAddons;
1784
+ }
1785
+ }
1786
+ }
1787
+ }
1788
+ }
1652
1789
  if (!process.env["CAMSTACK_NODE_ID"]) {
1653
1790
  process.env["CAMSTACK_NODE_ID"] = os3.hostname();
1654
1791
  }