nexarch 0.13.7 → 0.14.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.
@@ -344,7 +344,8 @@ export function canonicalTargetKey(filePath) {
344
344
  * consent from the dry-run result before calling again with `dryRun: false`
345
345
  * to actually write — see the ADR-0112 note at the call site.
346
346
  */
347
- function injectAgentConfigs(registry, runtimeCodes, dryRun) {
347
+ /** Exported for regression coverage of the empty-return bug documented at its tail. */
348
+ export function injectAgentConfigs(registry, runtimeCodes, dryRun) {
348
349
  const templateByCode = new Map(registry.instructionTemplates.map((t) => [t.code, t]));
349
350
  const sortedTargets = [...registry.instructionTargets]
350
351
  .filter((target) => target.matchMode === "exact")
@@ -437,7 +438,17 @@ function injectAgentConfigs(registry, runtimeCodes, dryRun) {
437
438
  if (perRuntimeResults.length > 0)
438
439
  return [...existingResults, ...perRuntimeResults.filter((result, index, arr) => arr.findIndex((item) => canonicalTargetKey(item.path) === canonicalTargetKey(result.path)) === index)];
439
440
  }
440
- return [];
441
+ // Not `[]`. When every runtime's target was already collected into
442
+ // `existingMatches` — which is the steady state once a repo has been set up
443
+ // once — the per-runtime loop `continue`s on all of them and
444
+ // `perRuntimeResults` comes back empty. Returning [] here threw away the
445
+ // results already computed above, the caller read that as "no instruction
446
+ // files" and fell back to injectGenericAgentConfig, and from then on only
447
+ // the generic target (AGENTS.md) was ever seen. Paul's CLAUDE.md and
448
+ // .cursorrules were invisible to staleness checking and attestation refresh
449
+ // from the second setup run onward, which is how they kept an expired
450
+ // `instruction_injection` block while the handshake reported all green.
451
+ return existingResults;
441
452
  }
442
453
  function trustAttestationStaleness(path) {
443
454
  if (!existsSync(path))
@@ -507,6 +518,45 @@ export function instructionInjectionDetail(registrationOk, staleAttestationTarge
507
518
  return `updated ${agentConfigResults.length} instruction target file(s)`;
508
519
  return "no runtime instruction target matched this repository (non-fatal; create AGENTS.md/CLAUDE.md or configure a generic target)";
509
520
  }
521
+ /**
522
+ * Every instruction file the registry recognises that actually exists in this
523
+ * repo, deduplicated the way the rest of this module keys paths.
524
+ *
525
+ * This is the denominator for the coverage guard below. It is deliberately
526
+ * computed straight from the registry and the filesystem rather than from
527
+ * whatever the injection path happened to return, because the bug it guards
528
+ * against was the injection path silently returning fewer files than exist.
529
+ */
530
+ export function existingInstructionFilePaths(registry) {
531
+ const seen = new Set();
532
+ const paths = [];
533
+ for (const target of registry.instructionTargets) {
534
+ if (target.matchMode !== "exact")
535
+ continue;
536
+ const filePath = join(process.cwd(), target.filePathPattern);
537
+ const key = canonicalTargetKey(filePath);
538
+ if (seen.has(key))
539
+ continue;
540
+ seen.add(key);
541
+ if (existsSync(filePath))
542
+ paths.push(filePath);
543
+ }
544
+ return paths;
545
+ }
546
+ /**
547
+ * Instruction files that exist on disk but were never evaluated for staleness.
548
+ *
549
+ * "already current (no refresh needed)" is only honest about files somebody
550
+ * looked at. When the injection path under-reports its targets, the attestation
551
+ * check evaluates a subset and reports green for the whole repo — which is
552
+ * exactly how two of Paul's three files kept an expired, wrongly-scoped block
553
+ * through a clean setup run. A non-empty result here fails the check rather
554
+ * than letting it speak for files it never opened.
555
+ */
556
+ export function unevaluatedInstructionFiles(onDisk, evaluated) {
557
+ const evaluatedKeys = new Set(evaluated.map((result) => canonicalTargetKey(result.path)));
558
+ return onDisk.filter((path) => !evaluatedKeys.has(canonicalTargetKey(path)));
559
+ }
510
560
  /**
511
561
  * Summarises the per-target attestation writes into the check's ok/detail.
512
562
  *
@@ -1481,6 +1531,9 @@ export async function initAgent(args) {
1481
1531
  : identityCapture.detail,
1482
1532
  });
1483
1533
  const instructionsAlreadyConfigured = agentConfigResults.length > 0 && agentConfigResults.every((r) => r.status === "already_present");
1534
+ const uncoveredInstructionFiles = registration.ok
1535
+ ? unevaluatedInstructionFiles(existingInstructionFilePaths(registry), agentConfigResults)
1536
+ : [];
1484
1537
  const attestationSummary = summariseAttestationOutcomes(attestationOutcomes);
1485
1538
  /** Renewed means every attested target verified on disk, not that one mint succeeded. */
1486
1539
  const attestationRenewed = attestationOutcomes.length > 0 && attestationSummary.ok;
@@ -1492,23 +1545,26 @@ export async function initAgent(args) {
1492
1545
  checks.push({
1493
1546
  name: "agent.trust.attestation",
1494
1547
  ok: !registration.ok || (unsafeReattestationTargetCount === 0
1548
+ && uncoveredInstructionFiles.length === 0
1495
1549
  && attestationSummary.ok
1496
1550
  && ((staleAttestationTargetCount === 0 && !trustAttestationAttempted) || Boolean(trustAttestation?.ok))),
1497
1551
  detail: !registration.ok
1498
1552
  ? "skipped (registration failed)"
1499
- : unsafeReattestationTargetCount > 0
1500
- ? "managed section does not match the published template; re-run with --allow-instruction-write"
1501
- : staleAttestationTargetCount > 0 && !attestationWriteAllowed
1502
- ? `${mismatchedAttestationTargetCount > 0 ? "mismatched" : "stale"}; re-run with --reattest`
1503
- : instructionsAlreadyConfigured && !attestationWriteAllowed
1504
- ? "skipped (already configured)"
1505
- : !attestationWriteAllowed
1506
- ? "skipped (consent not granted)"
1507
- : !trustAttestationAttempted
1508
- ? agentConfigResults.length > 0
1509
- ? "already current (no refresh needed)"
1510
- : "skipped (no instruction target written)"
1511
- : attestationSummary.detail,
1553
+ : uncoveredInstructionFiles.length > 0
1554
+ ? `${uncoveredInstructionFiles.length} instruction file(s) on disk were never evaluated (${uncoveredInstructionFiles.map((path) => basename(path)).join(", ")}); their attestations are unverified`
1555
+ : unsafeReattestationTargetCount > 0
1556
+ ? "managed section does not match the published template; re-run with --allow-instruction-write"
1557
+ : staleAttestationTargetCount > 0 && !attestationWriteAllowed
1558
+ ? `${mismatchedAttestationTargetCount > 0 ? "mismatched" : "stale"}; re-run with --reattest`
1559
+ : instructionsAlreadyConfigured && !attestationWriteAllowed
1560
+ ? "skipped (already configured)"
1561
+ : !attestationWriteAllowed
1562
+ ? "skipped (consent not granted)"
1563
+ : !trustAttestationAttempted
1564
+ ? agentConfigResults.length > 0
1565
+ ? "already current (no refresh needed)"
1566
+ : "skipped (no instruction target written)"
1567
+ : attestationSummary.detail,
1512
1568
  });
1513
1569
  checks.push({
1514
1570
  name: "technology.components",
@@ -24,15 +24,15 @@ async function promptCapabilityProfile() {
24
24
  return selectFromList("Select a capability profile for this credential:", PROFILE_CHOICES, "worker");
25
25
  }
26
26
  function printLoginBanner() {
27
- const logo = String.raw `
28
- ###### ######
29
- ####### ######
30
- ######### ###### ##
31
- ################## #####
32
- ##### ########## ######
33
- ###### ####### ######
34
- ##### #### #############
35
- ###### ## ###############
27
+ const logo = String.raw `
28
+ ###### ######
29
+ ####### ######
30
+ ######### ###### ##
31
+ ################## #####
32
+ ##### ########## ######
33
+ ###### ####### ######
34
+ ##### #### #############
35
+ ###### ## ###############
36
36
  `;
37
37
  console.log(logo);
38
38
  console.log("NEXARCH CLI login\n");
@@ -95,10 +95,10 @@ function waitForCallback(port, expectedState) {
95
95
  const companyName = url.searchParams.get("companyName") ?? undefined;
96
96
  const companyCode = url.searchParams.get("companyCode") ?? undefined;
97
97
  const error = url.searchParams.get("error");
98
- const html = (message) => `<!DOCTYPE html><html><head><meta charset="utf-8"><title>Nexarch CLI</title>
99
- <style>body{font-family:system-ui,sans-serif;display:flex;align-items:center;justify-content:center;min-height:100vh;margin:0;background:#f8fafc}
100
- .card{background:#fff;border:1px solid #e2e8f0;border-radius:12px;padding:2rem;max-width:400px;text-align:center}
101
- h1{font-size:1.25rem;margin:0 0 .5rem;color:#0f172a}p{color:#64748b;font-size:.9rem;margin:.5rem 0}</style></head>
98
+ const html = (message) => `<!DOCTYPE html><html><head><meta charset="utf-8"><title>Nexarch CLI</title>
99
+ <style>body{font-family:system-ui,sans-serif;display:flex;align-items:center;justify-content:center;min-height:100vh;margin:0;background:#f8fafc}
100
+ .card{background:#fff;border:1px solid #e2e8f0;border-radius:12px;padding:2rem;max-width:400px;text-align:center}
101
+ h1{font-size:1.25rem;margin:0 0 .5rem;color:#0f172a}p{color:#64748b;font-size:.9rem;margin:.5rem 0}</style></head>
102
102
  <body><div class="card"><h1>Nexarch CLI</h1><p>${message}</p></div></body></html>`;
103
103
  if (state !== expectedState) {
104
104
  res.writeHead(400, { "Content-Type": "text/html" });
@@ -247,6 +247,7 @@ export async function login(args) {
247
247
  companyCode: companyCode ?? undefined,
248
248
  companyId,
249
249
  expiresAt: expiresAt.toISOString(),
250
+ source: "interactive",
250
251
  });
251
252
  try {
252
253
  const identityDir = join(homedir(), ".nexarch");
@@ -37,24 +37,24 @@ export async function policyAuditTemplate(args) {
37
37
  const defaultResult = (parseOptionValue(args, "--default-result") ?? "fail").toLowerCase();
38
38
  const selectedControlIds = new Set(parseMultiOptionValues(args, "--control-id"));
39
39
  if (parseFlag(args, "--help") || parseFlag(args, "-h")) {
40
- console.log(`
41
- Usage:
42
- nexarch policy-audit-template --entity <application:key> [options]
43
-
44
- Options:
45
- --entity <key> Required (e.g. application:bad-driving)
46
- --control-id <uuid> Optional repeatable filter (selected controls only)
47
- --default-result <value> pass|partial|fail (default: fail)
48
- --output, --out <path.json> Write findings array to file
49
- --json Print JSON to stdout
50
-
51
- Output shape is ready for:
52
- nexarch policy-audit-submit --findings-file <path.json>
53
-
54
- Each entry includes empty severity/effort/exploitability/confidence/fileRefs
55
- placeholders. Fill them in for every partial/fail finding -- they decide how
56
- findings get ranked and reported. Leave a field blank/empty rather than
57
- guess; policy-audit-submit drops anything that isn't a valid value.
40
+ console.log(`
41
+ Usage:
42
+ nexarch policy-audit-template --entity <application:key> [options]
43
+
44
+ Options:
45
+ --entity <key> Required (e.g. application:bad-driving)
46
+ --control-id <uuid> Optional repeatable filter (selected controls only)
47
+ --default-result <value> pass|partial|fail (default: fail)
48
+ --output, --out <path.json> Write findings array to file
49
+ --json Print JSON to stdout
50
+
51
+ Output shape is ready for:
52
+ nexarch policy-audit-submit --findings-file <path.json>
53
+
54
+ Each entry includes empty severity/effort/exploitability/confidence/fileRefs
55
+ placeholders. Fill them in for every partial/fail finding -- they decide how
56
+ findings get ranked and reported. Leave a field blank/empty rather than
57
+ guess; policy-audit-submit drops anything that isn't a valid value.
58
58
  `);
59
59
  return;
60
60
  }
@@ -5,8 +5,8 @@ import { callMcpTool } from "../lib/mcp.js";
5
5
  export async function status(_args) {
6
6
  const creds = requireCredentials();
7
7
  const selectedCompanyId = creds.companyId;
8
- const expiresAt = new Date(creds.expiresAt);
9
- const daysLeft = Math.ceil((expiresAt.getTime() - Date.now()) / (1000 * 60 * 60 * 24));
8
+ const expiresAt = creds.expiresAt ? new Date(creds.expiresAt) : null;
9
+ const daysLeft = expiresAt ? Math.ceil((expiresAt.getTime() - Date.now()) / (1000 * 60 * 60 * 24)) : null;
10
10
  const lease = loadCheckInLease();
11
11
  const leaseState = lease && lease.companyId === selectedCompanyId ? getCheckInLeaseState(lease) : { status: "missing", expiresInSeconds: null };
12
12
  process.stdout.write("Connecting to mcp.nexarch.ai… ");
@@ -28,6 +28,15 @@ export async function status(_args) {
28
28
  if (creds.email) {
29
29
  console.log(` Signed in as: ${creds.email}`);
30
30
  }
31
+ else if (creds.source === "agent") {
32
+ console.log(" Authentication: Hermes agent enrollment");
33
+ if (creds.agentRef) {
34
+ console.log(` Agent: ${creds.agentRef}`);
35
+ }
36
+ }
37
+ else if (creds.source === "service") {
38
+ console.log(" Authentication: service credential");
39
+ }
31
40
  if (creds.company) {
32
41
  console.log(` Workspace: ${creds.company}`);
33
42
  }
@@ -53,7 +62,7 @@ export async function status(_args) {
53
62
  else {
54
63
  console.log(` Latest snapshot: none published yet`);
55
64
  }
56
- console.log(`\n Token expires: ${expiresAt.toLocaleDateString()} (${daysLeft} days)`);
65
+ console.log(`\n Token expires: ${expiresAt ? `${expiresAt.toLocaleDateString()} (${daysLeft} days)` : "not reported"}`);
57
66
  if (leaseState.status === "valid" && lease) {
58
67
  console.log(` Check-in lease: ${new Date(lease.leaseExpiresAt).toLocaleString()} (${Math.ceil((leaseState.expiresInSeconds ?? 0) / 3600)}h left)`);
59
68
  }
@@ -63,7 +72,7 @@ export async function status(_args) {
63
72
  else {
64
73
  console.log(" Check-in lease: none stored");
65
74
  }
66
- if (daysLeft <= 14) {
75
+ if (daysLeft !== null && daysLeft <= 14) {
67
76
  console.log(`\n ⚠ Your token expires soon. Run \`nexarch login\` to renew.`);
68
77
  }
69
78
  if (leaseState.status !== "valid") {
@@ -4,6 +4,9 @@ import { readFileSync, writeFileSync, mkdirSync, existsSync, rmSync } from "fs";
4
4
  function credentialsPath() {
5
5
  return join(homedir(), ".nexarch", "credentials.json");
6
6
  }
7
+ function agentCredentialPath() {
8
+ return join(homedir(), ".nexarch", "agent-credential.json");
9
+ }
7
10
  /**
8
11
  * Credentials supplied by the environment, for CI.
9
12
  *
@@ -27,24 +30,49 @@ function credentialsFromEnvironment() {
27
30
  company: "",
28
31
  companyId: process.env.NEXARCH_COMPANY_ID?.trim() ?? "",
29
32
  expiresAt: new Date(Date.now() + 60 * 60 * 1000).toISOString(),
33
+ source: "service",
30
34
  };
31
35
  }
36
+ function credentialsFromAgentManifest() {
37
+ const path = agentCredentialPath();
38
+ if (!existsSync(path))
39
+ return null;
40
+ try {
41
+ const manifest = JSON.parse(readFileSync(path, "utf-8"));
42
+ const token = typeof manifest.credential?.token === "string" ? manifest.credential.token.trim() : "";
43
+ const companyId = typeof manifest.workspace?.id === "string" ? manifest.workspace.id : "";
44
+ if (!token || !companyId)
45
+ return null;
46
+ return {
47
+ token,
48
+ email: "",
49
+ company: typeof manifest.workspace?.name === "string" ? manifest.workspace.name : "",
50
+ companyId,
51
+ expiresAt: typeof manifest.credential?.expiresAt === "string" ? manifest.credential.expiresAt : null,
52
+ source: "agent",
53
+ agentRef: typeof manifest.agent?.ref === "string" ? manifest.agent.ref : undefined,
54
+ };
55
+ }
56
+ catch {
57
+ return null;
58
+ }
59
+ }
32
60
  export function loadCredentials() {
33
61
  const fromEnv = credentialsFromEnvironment();
34
62
  if (fromEnv)
35
63
  return fromEnv;
36
64
  const path = credentialsPath();
37
65
  if (!existsSync(path))
38
- return null;
66
+ return credentialsFromAgentManifest();
39
67
  try {
40
68
  const raw = readFileSync(path, "utf-8");
41
69
  const creds = JSON.parse(raw);
42
70
  if (!creds.token)
43
- return null;
44
- return creds;
71
+ return credentialsFromAgentManifest();
72
+ return { ...creds, source: "interactive" };
45
73
  }
46
74
  catch {
47
- return null;
75
+ return credentialsFromAgentManifest();
48
76
  }
49
77
  }
50
78
  export function saveCredentials(creds) {
@@ -63,13 +91,13 @@ export function removeCredentials() {
63
91
  export function requireCredentials() {
64
92
  const creds = loadCredentials();
65
93
  if (!creds) {
66
- throw new Error("Not logged in. Run `nexarch login` to authenticate, or set NEXARCH_TOKEN for unattended use.");
94
+ throw new Error("No Nexarch credential found. Run `nexarch login` for interactive CLI access, or `nexarch enroll` for headless agent access.");
67
95
  }
68
96
  // Environment-supplied service credentials are validated by the gateway, not
69
97
  // by this process; only the stored interactive session carries a local expiry.
70
98
  if (process.env.NEXARCH_TOKEN?.trim())
71
99
  return creds;
72
- if (new Date(creds.expiresAt) < new Date()) {
100
+ if (creds.expiresAt && new Date(creds.expiresAt) < new Date()) {
73
101
  throw new Error("Your session has expired. Run `nexarch login` to re-authenticate.");
74
102
  }
75
103
  return creds;
package/package.json CHANGED
@@ -1,39 +1,39 @@
1
- {
2
- "name": "nexarch",
3
- "version": "0.13.7",
4
- "description": "Your architecture workspace for AI delivery.",
5
- "keywords": [
6
- "nexarch",
7
- "mcp",
8
- "architecture",
9
- "ai"
10
- ],
11
- "license": "MIT",
12
- "author": "Nexarch <hello@nexarch.ai>",
13
- "homepage": "https://nexarch.ai",
14
- "engines": {
15
- "node": ">=18"
16
- },
17
- "type": "module",
18
- "bin": {
19
- "nexarch": "dist/index.js"
20
- },
21
- "files": [
22
- "dist"
23
- ],
24
- "scripts": {
25
- "build": "tsc",
26
- "prepublishOnly": "tsc",
27
- "dev": "tsx src/index.ts",
28
- "typecheck": "tsc --noEmit",
29
- "test": "tsx scripts/test-mcp-proxy-response.ts && tsx scripts/test-trust-reattest.ts && tsx scripts/test-similar-applications.ts && tsx scripts/test-iac-tool.ts && tsx scripts/test-ansible.ts && tsx scripts/test-reference-sightings.ts && tsx scripts/test-search-icons.ts && tsx scripts/test-paste-login.ts"
30
- },
31
- "devDependencies": {
32
- "@types/node": "^22",
33
- "tsx": "^4",
34
- "typescript": "^5"
35
- },
36
- "dependencies": {
37
- "yaml": "^2.9.0"
38
- }
39
- }
1
+ {
2
+ "name": "nexarch",
3
+ "version": "0.14.0",
4
+ "description": "Your architecture workspace for AI delivery.",
5
+ "keywords": [
6
+ "nexarch",
7
+ "mcp",
8
+ "architecture",
9
+ "ai"
10
+ ],
11
+ "license": "MIT",
12
+ "author": "Nexarch <hello@nexarch.ai>",
13
+ "homepage": "https://nexarch.ai",
14
+ "engines": {
15
+ "node": ">=18"
16
+ },
17
+ "type": "module",
18
+ "bin": {
19
+ "nexarch": "dist/index.js"
20
+ },
21
+ "files": [
22
+ "dist"
23
+ ],
24
+ "scripts": {
25
+ "build": "tsc",
26
+ "prepublishOnly": "tsc",
27
+ "dev": "tsx src/index.ts",
28
+ "typecheck": "tsc --noEmit",
29
+ "test": "tsx scripts/test-mcp-proxy-response.ts && tsx scripts/test-trust-reattest.ts && tsx scripts/test-similar-applications.ts && tsx scripts/test-iac-tool.ts && tsx scripts/test-ansible.ts && tsx scripts/test-reference-sightings.ts && tsx scripts/test-search-icons.ts && tsx scripts/test-paste-login.ts && tsx scripts/test-credentials-resolution.ts"
30
+ },
31
+ "devDependencies": {
32
+ "@types/node": "^22",
33
+ "tsx": "^4",
34
+ "typescript": "^5"
35
+ },
36
+ "dependencies": {
37
+ "yaml": "^2.9.0"
38
+ }
39
+ }