@archme/cli 0.8.2 → 0.9.0

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.
Files changed (2) hide show
  1. package/dist/cli.js +87 -36
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -17151,6 +17151,7 @@ var init_localPoller = __esm({
17151
17151
  // src/daemon/run.ts
17152
17152
  var run_exports = {};
17153
17153
  __export(run_exports, {
17154
+ JOB_TOKEN_PREFIX: () => JOB_TOKEN_PREFIX,
17154
17155
  MultipleAccountsError: () => MultipleAccountsError,
17155
17156
  __testDispatch: () => __testDispatch,
17156
17157
  affectedFieldForComplete: () => affectedFieldForComplete,
@@ -17162,6 +17163,7 @@ __export(run_exports, {
17162
17163
  claudeFailureDetail: () => claudeFailureDetail,
17163
17164
  computeDiff: () => computeDiff,
17164
17165
  parseClaudeResult: () => parseClaudeResult,
17166
+ parseJobTokenResponse: () => parseJobTokenResponse,
17165
17167
  probeMcpAuth: () => probeMcpAuth,
17166
17168
  readBranchMarker: () => readBranchMarker,
17167
17169
  routeShutdownInterrupt: () => routeShutdownInterrupt,
@@ -17254,6 +17256,56 @@ async function getRepoCredentials(ctx, jobId, deviceId) {
17254
17256
  expires_at: typeof body.expires_at === "string" ? body.expires_at : null
17255
17257
  };
17256
17258
  }
17259
+ function parseJobTokenResponse(status, ok, body) {
17260
+ if (!ok) {
17261
+ throw new Error(`MCP job token unavailable: ${responseError(body, `job-token failed (${status})`)}`);
17262
+ }
17263
+ if (typeof body.token !== "string" || !body.token.startsWith(JOB_TOKEN_PREFIX) || body.token.length <= JOB_TOKEN_PREFIX.length) {
17264
+ throw new Error("MCP job token response did not carry an amj_ token");
17265
+ }
17266
+ const str2 = (v) => typeof v === "string" ? v : null;
17267
+ return {
17268
+ token: body.token,
17269
+ expires_at: str2(body.expires_at),
17270
+ project_id: str2(body.project_id),
17271
+ archme_branch: str2(body.archme_branch),
17272
+ scope: str2(body.scope)
17273
+ };
17274
+ }
17275
+ async function mintJobToken(ctx, jobId, deviceId) {
17276
+ const response = await fetchWithAuthRetry(
17277
+ "job-token",
17278
+ () => {
17279
+ const accessToken = ctx.auth.getAccessToken();
17280
+ if (!accessToken) throw new Error("daemon is not signed in with a registered device");
17281
+ return fetch(`${ctx.supabaseUrl.replace(/\/$/, "")}/functions/v1/agents-device/job-token`, {
17282
+ method: "POST",
17283
+ headers: {
17284
+ "Content-Type": "application/json",
17285
+ Authorization: `Bearer ${accessToken}`
17286
+ },
17287
+ body: JSON.stringify({
17288
+ device_id: deviceId,
17289
+ job_id: jobId
17290
+ })
17291
+ });
17292
+ },
17293
+ ctx.reLogin
17294
+ );
17295
+ const body = await parseJsonResponse(response);
17296
+ return parseJobTokenResponse(response.status, response.ok, body);
17297
+ }
17298
+ async function writeAgentMcpConfig(ctx, job, deviceId, projectSlug, source) {
17299
+ const { token } = await mintJobToken(ctx, job.id, deviceId);
17300
+ const { configPath, cleanup } = await writeMcpConfig({
17301
+ baseUrl: ctx.mcpBaseUrl,
17302
+ projectSlug,
17303
+ branch: job.archme_branch,
17304
+ bearer: token,
17305
+ source
17306
+ });
17307
+ return { configPath, cleanup, bearer: token };
17308
+ }
17257
17309
  async function buildGitEnv(credentials) {
17258
17310
  const dir = await mkdtemp3(join19(tmpdir3(), "archme-git-askpass-"));
17259
17311
  const askpassPath = join19(dir, "askpass.sh");
@@ -17805,7 +17857,7 @@ async function probeMcpAuth(mcpUrl, bearer, opts = {}) {
17805
17857
  body
17806
17858
  });
17807
17859
  lastStatus = res.status;
17808
- if (res.status !== 401) {
17860
+ if (res.status !== 401 && res.status !== 403) {
17809
17861
  return {
17810
17862
  ok: true,
17811
17863
  attempts: i2 + 1,
@@ -17813,13 +17865,13 @@ async function probeMcpAuth(mcpUrl, bearer, opts = {}) {
17813
17865
  detail: `MCP accepted the daemon token (status ${res.status})`
17814
17866
  };
17815
17867
  }
17816
- lastDetail = "MCP returned 401 \u2014 token rejected by JWKS verification";
17868
+ lastDetail = res.status === 403 ? "MCP returned 403 \u2014 job token rejected (job not running under this claim, or URL outside its binding)" : "MCP returned 401 \u2014 token rejected by JWKS verification";
17817
17869
  } catch (err2) {
17818
17870
  lastStatus = null;
17819
17871
  lastDetail = `network error reaching MCP: ${err2 instanceof Error ? err2.message : String(err2)}`;
17820
17872
  }
17821
17873
  }
17822
- if (lastStatus === 401) {
17874
+ if (lastStatus === 401 || lastStatus === 403) {
17823
17875
  return { ok: false, attempts, lastStatus, detail: lastDetail };
17824
17876
  }
17825
17877
  return {
@@ -17894,19 +17946,12 @@ function parseClaudeResult(stdout) {
17894
17946
  return { summary: raw, modelServed: null, usage: null, costUsd: null, isError: false, parsed: false };
17895
17947
  }
17896
17948
  }
17897
- async function setupDigestMcp(ctx, job) {
17949
+ async function setupDigestMcp(ctx, job, deviceId) {
17898
17950
  let cleanup;
17899
17951
  try {
17952
+ if (!deviceId) throw new Error("daemon is not signed in with a registered device");
17900
17953
  const projectSlug = await lookupProjectSlug(ctx.supabase, job.project_id);
17901
- const mcpBearer = await ctx.auth.getMcpAccessToken();
17902
- const { configPath, cleanup: cleanupFn } = await writeMcpConfig({
17903
- baseUrl: ctx.mcpBaseUrl,
17904
- projectSlug,
17905
- branch: job.archme_branch,
17906
- bearer: mcpBearer
17907
- // No `source`: digest reads must not produce source-tagged commits — they
17908
- // don't write at all, but leaving it off keeps the URL read-shaped.
17909
- });
17954
+ const { configPath, cleanup: cleanupFn, bearer: mcpBearer } = await writeAgentMcpConfig(ctx, job, deviceId, projectSlug);
17910
17955
  cleanup = cleanupFn;
17911
17956
  const probe = await probeMcpAuth(
17912
17957
  `${ctx.mcpBaseUrl}?project=${encodeURIComponent(projectSlug)}&branch=${encodeURIComponent(job.archme_branch)}`,
@@ -17929,7 +17974,7 @@ async function setupDigestMcp(ctx, job) {
17929
17974
  async function runJob(ctx, job, claimDeviceId) {
17930
17975
  if (job.job_kind === "digest") {
17931
17976
  const digestDevice = claimDeviceId ?? ctx.deviceForOrg(job.org_id) ?? ctx.auth.getDeviceId() ?? void 0;
17932
- const { mcp, cleanup } = await setupDigestMcp(ctx, job);
17977
+ const { mcp, cleanup } = await setupDigestMcp(ctx, job, digestDevice);
17933
17978
  try {
17934
17979
  await runDigestJob(ctx, job, digestDevice, inFlight, mcp);
17935
17980
  } finally {
@@ -17961,6 +18006,27 @@ async function runJob(ctx, job, claimDeviceId) {
17961
18006
  }, HEARTBEAT_INTERVAL_MS);
17962
18007
  try {
17963
18008
  const projectSlug = await lookupProjectSlug(ctx.supabase, job.project_id);
18009
+ const { configPath, cleanup: cleanupFn, bearer: mcpBearer } = await writeAgentMcpConfig(
18010
+ ctx,
18011
+ job,
18012
+ deviceId,
18013
+ projectSlug,
18014
+ {
18015
+ repo: `${job.repo_owner}/${job.repo_name}`,
18016
+ before_sha: job.before_sha ?? null,
18017
+ after_sha: job.after_sha,
18018
+ job_id: job.id,
18019
+ // Effective run mode — always diff or pr_update now (reconcile is gone).
18020
+ mode: job.source_mode === "pr_update" ? "pr_update" : "diff",
18021
+ commit_message: job.commit_message ?? null,
18022
+ committed_at: job.head_commit_timestamp ?? null,
18023
+ author_name: job.author_name ?? null,
18024
+ author_login: job.author_login ?? null
18025
+ }
18026
+ );
18027
+ activeCleanups.add(cleanupFn);
18028
+ cleanupFns.push(cleanupFn);
18029
+ markPhase("job_token");
17964
18030
  let clone;
17965
18031
  try {
17966
18032
  clone = await ensureCloneAndFetch(ctx, job, deviceId);
@@ -18041,27 +18107,6 @@ async function runJob(ctx, job, claimDeviceId) {
18041
18107
  throw new Error(`failed to materialize large diff to disk (${bytes} bytes): ${e.message}`);
18042
18108
  }
18043
18109
  }
18044
- const mcpBearer = await ctx.auth.getMcpAccessToken();
18045
- const { configPath, cleanup: cleanupFn } = await writeMcpConfig({
18046
- baseUrl: ctx.mcpBaseUrl,
18047
- projectSlug,
18048
- branch: job.archme_branch,
18049
- bearer: mcpBearer,
18050
- source: {
18051
- repo: `${job.repo_owner}/${job.repo_name}`,
18052
- before_sha: job.before_sha ?? null,
18053
- after_sha: job.after_sha,
18054
- job_id: job.id,
18055
- // Effective run mode — always diff or pr_update now (reconcile is gone).
18056
- mode: effectiveMode,
18057
- commit_message: job.commit_message ?? null,
18058
- committed_at: job.head_commit_timestamp ?? null,
18059
- author_name: job.author_name ?? null,
18060
- author_login: job.author_login ?? null
18061
- }
18062
- });
18063
- activeCleanups.add(cleanupFn);
18064
- cleanupFns.push(cleanupFn);
18065
18110
  let affectedSeed = "";
18066
18111
  let affectedForComplete = null;
18067
18112
  if (diff) {
@@ -18674,7 +18719,7 @@ ${signal} received, shutting down`);
18674
18719
  process.on("SIGINT", () => shutdown("SIGINT"));
18675
18720
  process.on("SIGTERM", () => shutdown("SIGTERM"));
18676
18721
  }
18677
- var HEARTBEAT_INTERVAL_MS, CLAUDE_TIMEOUT_MS, CLAUDE_LARGE_DIFF_TIMEOUT_MS, SAFETY_SWEEP_MS, LOG_STDOUT_CAP_BYTES, LOG_STDERR_CAP_BYTES, FAILURE_DETAIL_MAX, PRE_SPAWN_TOKEN_MIN_LIFETIME_MS, SUPABASE_URL, SUPABASE_ANON_KEY, ARCHME_DAEMON_CLIENT_ID, ARCHME_MCP_BASE_URL, CLONE_BASE_DIR, DAEMON_ID, LEASE_SECONDS, inFlight, cancelled, killReasons, activeCleanups, cloneMutex, shuttingDown, daemonVersion, pauseUntilMs, resumeTimer, currentSched, MAX_CONCURRENT_JOBS, ctxByJob, deviceByJob, DEFAULT_CLONE_BASE_DIR, INLINE_PATCH_BYTE_CAP, GIT_SHA_RE, CURATOR_PREAMBLE, CORE_PRINCIPLES, STORY_COVERAGE_GATE, NEW_STORY_GUIDANCE, STORY_DESCRIPTION_EDIT_RULE, PHASE_GUIDANCE, BASE_RULES, DIFF_RULES, finite, channelSeq, MultipleAccountsError, __testDispatch;
18722
+ var HEARTBEAT_INTERVAL_MS, CLAUDE_TIMEOUT_MS, CLAUDE_LARGE_DIFF_TIMEOUT_MS, SAFETY_SWEEP_MS, LOG_STDOUT_CAP_BYTES, LOG_STDERR_CAP_BYTES, FAILURE_DETAIL_MAX, PRE_SPAWN_TOKEN_MIN_LIFETIME_MS, SUPABASE_URL, SUPABASE_ANON_KEY, ARCHME_DAEMON_CLIENT_ID, ARCHME_MCP_BASE_URL, CLONE_BASE_DIR, DAEMON_ID, LEASE_SECONDS, inFlight, cancelled, killReasons, activeCleanups, cloneMutex, shuttingDown, daemonVersion, pauseUntilMs, resumeTimer, currentSched, MAX_CONCURRENT_JOBS, ctxByJob, deviceByJob, DEFAULT_CLONE_BASE_DIR, JOB_TOKEN_PREFIX, INLINE_PATCH_BYTE_CAP, GIT_SHA_RE, CURATOR_PREAMBLE, CORE_PRINCIPLES, STORY_COVERAGE_GATE, NEW_STORY_GUIDANCE, STORY_DESCRIPTION_EDIT_RULE, PHASE_GUIDANCE, BASE_RULES, DIFF_RULES, finite, channelSeq, MultipleAccountsError, __testDispatch;
18678
18723
  var init_run = __esm({
18679
18724
  "src/daemon/run.ts"() {
18680
18725
  "use strict";
@@ -18736,6 +18781,7 @@ var init_run = __esm({
18736
18781
  ctxByJob = /* @__PURE__ */ new Map();
18737
18782
  deviceByJob = /* @__PURE__ */ new Map();
18738
18783
  DEFAULT_CLONE_BASE_DIR = `${homedir6()}/.archme-job-runner/clones`;
18784
+ JOB_TOKEN_PREFIX = "amj_";
18739
18785
  INLINE_PATCH_BYTE_CAP = 2e5;
18740
18786
  GIT_SHA_RE = /^[0-9a-f]{40}$/;
18741
18787
  CURATOR_PREAMBLE = `## Your role
@@ -18864,6 +18910,11 @@ ${ANCHOR_VERIFICATION}`;
18864
18910
  return deviceByJob.get(jobId);
18865
18911
  },
18866
18912
  runJob,
18913
+ // R7: the job-bound `amj_` MCP token mint and the digest's MCP setup, so a
18914
+ // test can assert the agent's mcp.json carries the job token and never the
18915
+ // daemon's own session.
18916
+ mintJobToken,
18917
+ setupDigestMcp,
18867
18918
  // Exposes the clone/fetch entrypoint so a test can assert that the repo-token
18868
18919
  // mint (getRepoCredentials -> agents-device/repo-token, gated on
18869
18920
  // claimed_by_device_id) uses the CLAIM-time device rather than recomputing via
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@archme/cli",
3
- "version": "0.8.2",
3
+ "version": "0.9.0",
4
4
  "description": "Map your codebase into a living architecture diagram: run `archme init` in a repo to scan it, `archme daemon start` to keep it current on every commit.",
5
5
  "type": "module",
6
6
  "license": "SEE LICENSE IN LICENSE",