@proagentstore/cli 0.4.91 → 0.4.93

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.
@@ -20,6 +20,8 @@ export const LOCAL_ARTIFACT_TASK_TYPE = "local_artifact.generate";
20
20
  export const LOCAL_ARTIFACT_RUN_PATH = "/local-artifact/run";
21
21
  export const LOCAL_ARTIFACT_STATUS_PATH = "/local-artifact/status";
22
22
  export const LOCAL_ARTIFACT_CANCEL_PATH = "/local-artifact/cancel";
23
+ /** First CLI that consumes an explicit uploaded source instead of its local master files. */
24
+ export const LOCAL_ARTIFACT_UPLOADED_SOURCES_MIN_CLI = "0.4.93";
23
25
  /** The readiness event a completed tailoring run emits, once, through the connection outbox. */
24
26
  export const MATERIALS_READY_EVENT = "job.application.materials_ready";
25
27
  export const LOCAL_ARTIFACT_ENGINES = ["claude", "codex"];
@@ -67,6 +67,26 @@ export function parseArtifactEnvelope(raw) {
67
67
  throw bad(`source ${String(r.role)} is listed twice`);
68
68
  sources.push({ role: r.role, path: r.path });
69
69
  }
70
+ const uploadedSources = [];
71
+ if (o.uploadedSources !== undefined) {
72
+ if (!Array.isArray(o.uploadedSources) || o.uploadedSources.length !== 2)
73
+ throw bad("uploadedSources must contain the selected resume and profile");
74
+ for (const source of o.uploadedSources) {
75
+ const r = (source && typeof source === "object" ? source : {});
76
+ if ((r.role !== "resume" && r.role !== "profile") || uploadedSources.some((x) => x.role === r.role))
77
+ throw bad("uploaded sources need one resume and one profile");
78
+ if (typeof r.fileId !== "string" || !PATH_SEGMENT.test(r.fileId) || typeof r.version !== "string" || !r.version || r.version.length > 300 || typeof r.originalSha256 !== "string" || !/^[a-f0-9]{64}$/.test(r.originalSha256) || typeof r.extractedTextSha256 !== "string" || !/^[a-f0-9]{64}$/.test(r.extractedTextSha256) || typeof r.extractedAt !== "string" || Number.isNaN(Date.parse(r.extractedAt)) || typeof r.text !== "string")
79
+ throw bad("uploaded source provenance is invalid");
80
+ const bytes = Buffer.byteLength(r.text, "utf8");
81
+ if (!bytes || bytes > LOCAL_ARTIFACT_CAPS.sourceBytes)
82
+ throw bad(`uploaded source must be 1-${LOCAL_ARTIFACT_CAPS.sourceBytes} UTF-8 bytes`);
83
+ if (sha256(r.text) !== r.extractedTextSha256)
84
+ throw bad("uploaded source text does not match its extracted-text hash");
85
+ uploadedSources.push({ role: r.role, fileId: r.fileId, version: r.version, originalSha256: r.originalSha256, extractedTextSha256: r.extractedTextSha256, extractedAt: r.extractedAt, text: r.text });
86
+ }
87
+ if (uploadedSources.length !== 2 || !uploadedSources.some((s) => s.role === "resume") || !uploadedSources.some((s) => s.role === "profile"))
88
+ throw bad("uploaded sources need one resume and one profile");
89
+ }
70
90
  const p = (o.policy && typeof o.policy === "object" ? o.policy : {});
71
91
  const retainDays = intIn(p.retainDays, 0, 3650);
72
92
  const maxMinutes = intIn(p.maxMinutes, 1, 60);
@@ -83,6 +103,7 @@ export function parseArtifactEnvelope(raw) {
83
103
  authMode: o.authMode,
84
104
  workspace: o.workspace,
85
105
  sources,
106
+ ...(uploadedSources.length ? { uploadedSources } : {}),
86
107
  policy: { retainDays, maxMinutes, maxConcurrent },
87
108
  ...("lead" in lead ? { lead: lead.lead } : { lead: null, leadError: lead.error }),
88
109
  };
@@ -163,31 +184,53 @@ export class LocalArtifactRuntime {
163
184
  throw new Pause("workspace_unavailable", [`The workspace ${e.workspace} cannot be used: ${err instanceof Error ? err.message : String(err)}`]);
164
185
  }
165
186
  this.sweepArtifacts(run, workspace);
166
- // Sources: read by the runtime, hashed, handed to the CLI in its prompt. Content never leaves this function except into the prompt.
187
+ // Sources: uploaded selections arrive only over the authenticated relay, are written beneath
188
+ // this run's private scratch folder under fixed role names, and never use a user filename/path.
189
+ // Local mode remains the existing path. Content never reaches events/results/DB.
167
190
  const texts = [];
168
191
  const hashes = [];
169
192
  const missing = [];
170
- for (const s of e.sources) {
171
- const shown = `${e.workspace}/${s.path}`;
172
- try {
173
- const file = resolveSource(workspace, s.path);
174
- const buf = readFileSync(file);
175
- if (buf.length > LOCAL_ARTIFACT_CAPS.sourceBytes)
176
- throw new Error(`larger than ${LOCAL_ARTIFACT_CAPS.sourceBytes / 1024} KB`);
177
- if (!buf.length)
178
- throw new Error("empty");
179
- texts.push({ role: s.role, text: buf.toString("utf8") });
180
- const hash = { role: s.role, path: shown, sha256: sha256(buf), bytes: buf.length };
181
- hashes.push(hash);
182
- this.emit(run, { type: "source.read", detail: hash });
183
- }
184
- catch (err) {
185
- this.emit(run, { type: "source.missing", detail: { role: s.role, path: shown } });
186
- missing.push(`Your ${s.role} at ${shown} could not be read (${err instanceof Error && !/ENOENT/.test(err.message) ? err.message : "not found"}). Add it, or choose another file in the Application Tailor settings.`);
193
+ if (e.uploadedSources?.length) {
194
+ const sourceDir = join(run.scratch, "sources");
195
+ mkdirSync(sourceDir, { recursive: true, mode: 0o700 });
196
+ for (const s of e.uploadedSources) {
197
+ const safePath = join(sourceDir, `${s.role}.txt`);
198
+ try {
199
+ writeFileSync(safePath, s.text, { encoding: "utf8", flag: "wx", mode: 0o600 });
200
+ const bytes = Buffer.byteLength(s.text, "utf8");
201
+ const hash = { role: s.role, path: `uploaded/${s.fileId}@${s.version}`, sha256: s.extractedTextSha256, bytes };
202
+ texts.push({ role: s.role, text: readFileSync(safePath, "utf8") });
203
+ hashes.push(hash);
204
+ this.emit(run, { type: "source.read", detail: hash });
205
+ }
206
+ catch (err) {
207
+ this.emit(run, { type: "source.missing", detail: { role: s.role, path: `uploaded/${s.fileId}` } });
208
+ missing.push(`The selected uploaded ${s.role} could not be materialized (${err instanceof Error ? err.message : "unknown error"}).`);
209
+ }
187
210
  }
188
211
  }
212
+ else
213
+ for (const s of e.sources) {
214
+ const shown = `${e.workspace}/${s.path}`;
215
+ try {
216
+ const file = resolveSource(workspace, s.path);
217
+ const buf = readFileSync(file);
218
+ if (buf.length > LOCAL_ARTIFACT_CAPS.sourceBytes)
219
+ throw new Error(`larger than ${LOCAL_ARTIFACT_CAPS.sourceBytes / 1024} KB`);
220
+ if (!buf.length)
221
+ throw new Error("empty");
222
+ texts.push({ role: s.role, text: buf.toString("utf8") });
223
+ const hash = { role: s.role, path: shown, sha256: sha256(buf), bytes: buf.length };
224
+ hashes.push(hash);
225
+ this.emit(run, { type: "source.read", detail: hash });
226
+ }
227
+ catch (err) {
228
+ this.emit(run, { type: "source.missing", detail: { role: s.role, path: shown } });
229
+ missing.push(`Your ${s.role} at ${shown} could not be read (${err instanceof Error && !/ENOENT/.test(err.message) ? err.message : "not found"}). Add it, or choose another file in the Application Tailor settings.`);
230
+ }
231
+ }
189
232
  for (const role of REQUIRED_SOURCE_ROLES) {
190
- if (!e.sources.some((s) => s.role === role))
233
+ if (e.uploadedSources?.length ? !e.uploadedSources.some((s) => s.role === role) : !e.sources.some((s) => s.role === role))
191
234
  missing.push(`No ${role} source is configured. Choose one in the Application Tailor settings.`);
192
235
  }
193
236
  if (missing.length)
package/dist/index.js CHANGED
@@ -1716,6 +1716,152 @@ function mayAutomaticallyRestart(input) {
1716
1716
  return input.authoritative && input.enabled && input.workObserved && input.busy.length === 0;
1717
1717
  }
1718
1718
 
1719
+ // src/commands/runner/auto-update-controller.ts
1720
+ var AutomaticUpdateController = class {
1721
+ constructor(deps) {
1722
+ this.deps = deps;
1723
+ this.setTimer = deps.setTimer ?? ((fn, ms) => setTimeout(fn, ms));
1724
+ this.clearTimer = deps.clearTimer ?? ((timer) => clearTimeout(timer));
1725
+ }
1726
+ deps;
1727
+ timer = null;
1728
+ checking = false;
1729
+ waiting = false;
1730
+ installing = false;
1731
+ failures = 0;
1732
+ setTimer;
1733
+ clearTimer;
1734
+ /** Called only for a true policy transition. `keep` deliberately leaves a due heartbeat timer alone. */
1735
+ onPolicy(action) {
1736
+ if (action === "cancel") return this.cancel();
1737
+ if (action === "start") this.schedule(true);
1738
+ }
1739
+ /** Testable immediate entry point; production reaches it through the scheduled timer. */
1740
+ async runNow() {
1741
+ const current = this.deps.policy();
1742
+ if (this.checking || this.waiting || !current.authoritative || !current.value.autoUpdate) return;
1743
+ this.checking = true;
1744
+ try {
1745
+ if (!await this.deps.refreshPolicy() || !this.enabled()) {
1746
+ this.deps.status("offline", { reason: "The owner policy could not be refreshed; automatic update is deferred." });
1747
+ return;
1748
+ }
1749
+ await this.decide(false);
1750
+ } catch (error) {
1751
+ this.fail(error);
1752
+ } finally {
1753
+ this.checking = false;
1754
+ }
1755
+ }
1756
+ /** Explicit remote updates and unattended checks share this single flight. */
1757
+ async installManually(plan) {
1758
+ return this.install(plan, false);
1759
+ }
1760
+ enabled() {
1761
+ const { value, authoritative } = this.deps.policy();
1762
+ return authoritative && value.autoUpdate;
1763
+ }
1764
+ schedule(initial = false) {
1765
+ if (!this.enabled() || this.timer || this.waiting) return;
1766
+ const delay = initial ? Math.round(5e3 + (this.deps.random ?? Math.random)() * 25e3) : nextAutoUpdateDelayMs(this.failures, this.deps.random);
1767
+ this.timer = this.setTimer(() => {
1768
+ this.timer = null;
1769
+ void this.runNow();
1770
+ }, delay);
1771
+ this.timer.unref?.();
1772
+ }
1773
+ cancel() {
1774
+ if (this.timer) this.clearTimer(this.timer);
1775
+ this.timer = null;
1776
+ }
1777
+ fail(error) {
1778
+ this.failures++;
1779
+ this.deps.status("failure", { reason: error instanceof Error ? error.message : String(error) });
1780
+ this.schedule();
1781
+ }
1782
+ async decide(deferred) {
1783
+ const facts = await this.deps.facts();
1784
+ if (!this.enabled()) return;
1785
+ const plan = this.deps.plan(facts);
1786
+ if (plan.action === "up-to-date") {
1787
+ this.failures = 0;
1788
+ this.deps.status("verified-success", { latestVersion: facts.latest ?? void 0, reason: void 0 });
1789
+ this.schedule();
1790
+ return;
1791
+ }
1792
+ if (plan.action === "refused") {
1793
+ this.deps.status("unsupported", { reason: plan.reason });
1794
+ this.schedule();
1795
+ return;
1796
+ }
1797
+ if (plan.action === "wait") {
1798
+ this.deps.status("waiting-for-idle", { latestVersion: plan.latest, reason: void 0 });
1799
+ this.defer();
1800
+ return;
1801
+ }
1802
+ this.deps.status("running", { latestVersion: plan.latest, reason: void 0 });
1803
+ const restarted = await this.install(plan, true);
1804
+ if (restarted) this.deps.status("restarting", { latestVersion: plan.latest, reason: void 0 });
1805
+ else if (!deferred) this.defer(plan.latest);
1806
+ }
1807
+ defer(latest) {
1808
+ if (this.waiting) return;
1809
+ this.waiting = true;
1810
+ this.timer = this.setTimer(() => {
1811
+ this.timer = null;
1812
+ void (async () => {
1813
+ this.waiting = false;
1814
+ try {
1815
+ if (!await this.deps.refreshPolicy() || !this.enabled()) {
1816
+ this.deps.status("offline", { reason: "The owner policy could not be refreshed; automatic update is deferred." });
1817
+ return;
1818
+ }
1819
+ await this.decide(true);
1820
+ } catch (error) {
1821
+ this.fail(error);
1822
+ }
1823
+ })();
1824
+ }, 15e3);
1825
+ this.timer.unref?.();
1826
+ this.deps.status("waiting-for-idle", latest ? { latestVersion: latest } : void 0);
1827
+ }
1828
+ async install(plan, automatic) {
1829
+ if (this.installing) return false;
1830
+ this.installing = true;
1831
+ try {
1832
+ return await this.deps.installAndRestart(plan, automatic);
1833
+ } finally {
1834
+ this.installing = false;
1835
+ }
1836
+ }
1837
+ };
1838
+ var RelayMutationAdmission = class {
1839
+ draining = false;
1840
+ inFlight = 0;
1841
+ waiters = [];
1842
+ begin() {
1843
+ if (this.draining) return null;
1844
+ this.inFlight++;
1845
+ let released = false;
1846
+ return () => {
1847
+ if (released) return;
1848
+ released = true;
1849
+ this.inFlight--;
1850
+ if (this.inFlight === 0) {
1851
+ for (const wake of this.waiters.splice(0)) wake();
1852
+ }
1853
+ };
1854
+ }
1855
+ async drain() {
1856
+ this.draining = true;
1857
+ if (this.inFlight === 0) return;
1858
+ await new Promise((resolve5) => this.waiters.push(resolve5));
1859
+ }
1860
+ resume() {
1861
+ this.draining = false;
1862
+ }
1863
+ };
1864
+
1719
1865
  // src/commands/runner/relay.ts
1720
1866
  var MEMBERSHIP_SYNC_PATH = "/pags/membership/sync";
1721
1867
  var CLI_CONTROL_PATHS = /* @__PURE__ */ new Set([MEMBERSHIP_SYNC_PATH, RUNNER_UPDATE_PATH]);
@@ -1839,7 +1985,7 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force =
1839
1985
  };
1840
1986
  let updateInstallInFlight = false;
1841
1987
  let installedPendingRestart = null;
1842
- let automaticRestartDraining = false;
1988
+ const relayMutationAdmission = new RelayMutationAdmission();
1843
1989
  const installAndRestart = async (plan, automatic = false) => {
1844
1990
  if (updateInstallInFlight) return false;
1845
1991
  updateInstallInFlight = true;
@@ -1858,17 +2004,17 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force =
1858
2004
  }
1859
2005
  }
1860
2006
  if (automatic) {
1861
- automaticRestartDraining = true;
2007
+ await relayMutationAdmission.drain();
1862
2008
  await new Promise((resolve5) => setTimeout(resolve5, 500));
1863
2009
  if (!await refreshAutomaticPolicy() || !autoUpdatePolicy.autoUpdate) {
1864
- automaticRestartDraining = false;
2010
+ relayMutationAdmission.resume();
1865
2011
  updateInstallInFlight = false;
1866
2012
  return false;
1867
2013
  }
1868
2014
  }
1869
2015
  const finalFacts = await updateFacts();
1870
2016
  if (finalFacts.busy.length || automatic && !mayAutomaticallyRestart({ authoritative: autoUpdatePolicyAuthoritative, enabled: autoUpdatePolicy.autoUpdate, workObserved: !finalFacts.busy.includes("runner-work-observation-unavailable"), busy: finalFacts.busy })) {
1871
- automaticRestartDraining = false;
2017
+ relayMutationAdmission.resume();
1872
2018
  updateInstallInFlight = false;
1873
2019
  return false;
1874
2020
  }
@@ -1985,6 +2131,17 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force =
1985
2131
  }, delay);
1986
2132
  automaticTimer.unref();
1987
2133
  };
2134
+ const automaticUpdateController = new AutomaticUpdateController({
2135
+ policy: () => ({ value: autoUpdatePolicy, authoritative: autoUpdatePolicyAuthoritative }),
2136
+ refreshPolicy: refreshAutomaticPolicy,
2137
+ facts: updateFacts,
2138
+ plan: planRunnerUpdate,
2139
+ // This bridge is deliberately the same function used by explicit runner_update. It owns npm,
2140
+ // a fresh authoritative policy, the mutation drain, final local-work observation and restart.
2141
+ installAndRestart,
2142
+ status: saveAutoUpdateStatus
2143
+ });
2144
+ scheduleAutomaticUpdate = (initial = false, cancel = false) => automaticUpdateController.onPolicy(cancel ? "cancel" : initial ? "start" : "keep");
1988
2145
  if (autoUpdatePolicyAuthoritative && autoUpdatePolicy.autoUpdate) scheduleAutomaticUpdate(true);
1989
2146
  const answerUpdate = async (body) => {
1990
2147
  const dryRun = body?.dryRun === true;
@@ -1995,7 +2152,7 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force =
1995
2152
  return { status: 200, result: { ...plan, detail: "Restarts itself as soon as these engines finish their turns \u2014 no run is cut off." } };
1996
2153
  }
1997
2154
  try {
1998
- if (!await installAndRestart(plan)) return { status: 409, result: { error: "A runner update is already installing or restarting." } };
2155
+ if (!await automaticUpdateController.installManually(plan)) return { status: 409, result: { error: "A runner update is already installing or restarting." } };
1999
2156
  } catch (e) {
2000
2157
  return { status: 500, result: { error: `npm could not install ${plan.latest}: ${e instanceof Error ? e.message : String(e)}` } };
2001
2158
  }
@@ -2064,7 +2221,7 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force =
2064
2221
  runnerNode,
2065
2222
  // The final automatic-update drain is an admission gate, not merely another poll:
2066
2223
  // command work starts only when the old runner is still permitted to serve it.
2067
- () => !automaticRestartDraining
2224
+ () => relayMutationAdmission.begin()
2068
2225
  )
2069
2226
  );
2070
2227
  if (label) writeLine(`Attached agent: ${label}`);
@@ -2208,7 +2365,7 @@ async function readSessionResources(localUrl, runnerToken) {
2208
2365
  }
2209
2366
  var RTT_PROBE_PREFIX = "rtt:";
2210
2367
  var RTT_ECHO_PREFIX = "rtt-echo:";
2211
- function openRelaySocket(instanceId, wsBase, mintToken, localUrl, runnerToken, force = false, onConflict, onOpen, onControl, runnerNode = hostname3(), admitMutation = () => true) {
2368
+ function openRelaySocket(instanceId, wsBase, mintToken, localUrl, runnerToken, force = false, onConflict, onOpen, onControl, runnerNode = hostname3(), beginMutation = () => () => void 0) {
2212
2369
  let backoffMs = 1e3;
2213
2370
  let reconnecting = false;
2214
2371
  let closed = false;
@@ -2276,7 +2433,8 @@ function openRelaySocket(instanceId, wsBase, mintToken, localUrl, runnerToken, f
2276
2433
  return;
2277
2434
  }
2278
2435
  const method = (cmd.method || "POST").toUpperCase();
2279
- if (method !== "GET" && method !== "HEAD" && !admitMutation()) {
2436
+ const releaseMutation = method !== "GET" && method !== "HEAD" ? beginMutation() : null;
2437
+ if (method !== "GET" && method !== "HEAD" && !releaseMutation) {
2280
2438
  try {
2281
2439
  ws.send(JSON.stringify({ id: cmd.id, status: 503, result: { error: "Runner is draining for a safe automatic update; retry shortly." } }));
2282
2440
  } catch {
@@ -2310,6 +2468,8 @@ function openRelaySocket(instanceId, wsBase, mintToken, localUrl, runnerToken, f
2310
2468
  ws.send(JSON.stringify({ id: cmd.id, status: 500, error: err instanceof Error ? err.message : String(err) }));
2311
2469
  } catch {
2312
2470
  }
2471
+ } finally {
2472
+ releaseMutation?.();
2313
2473
  }
2314
2474
  };
2315
2475
  ws.onclose = (ev) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@proagentstore/cli",
3
- "version": "0.4.91",
3
+ "version": "0.4.93",
4
4
  "description": "CLI for creating, publishing, and running ProAgentStore agents",
5
5
  "license": "MIT",
6
6
  "type": "module",